# Manual Installation

> Install Broadcast manually on your Ubuntu server with the official Docker image: set env variables, PostgreSQL, and a reverse proxy step by step.

Source: https://sendbroadcast.net/docs/manual-installation

### Warning: Gotcha ahead!

Although we provide a guide below, please note that manual installation is not supported due to the variety of ways that people run their servers. You may, however, still reach out to us and we can offer general guidance.

Broadcast can also be installed manually on your server with the Docker image.

This method is more complex than the automation installation method, and so this is not recommended for most users. However, there are specific reasons why you might want to manually install and run Docker.

First, let's talk about what functionalities are not available with the manual installation method.

## Features not available with manual installation

### 1. Auto updates

You will need to manually update the Docker image to get the latest features and security updates. See below for examples on how to do this manually.

### 2. Backups

Automatic backups are not supported as this feature requires creating system services. You will need to manually backup the database or set up some script to do it for you. Below, we provide you with some example scripts to help you get started.

### 3. Reverse proxy / multi-domains / SSL termination

You can still use multiple domains on a single server with Broadcast, but you will need to manually set up your server's reverse proxy to handle this for you.

For example, if you are using Nginx or Caddy, you will need to manually set up the reverse proxy to handle the multiple domains.

You will also need to manually set up your SSL certificates.

## Steps to manually install and run Broadcast

### 1. Create a directory to run Broadcast

Clone the Broadcast script file to your server:

```bash
git clone https://github.com/send-broadcast/broadcast-script.git /home/your-user/broadcast
```

Replace `your-user` with the username of the user account on your server.

Be careful to not run the `broadcast.sh` script, as that is used for the automation installation method.

### 2. Modify the docker-compose.manual.yml file

There is a `docker-compose.manual.yml` file in the cloned repository.

The file looks like this:

```yaml
# docker-compose.manual.yml
services:
  app:
    image: ${DOCKER_IMAGE:-gitea.hostedapp.org/broadcast/broadcast:latest}
    platform: linux/${TARGETARCH:-amd64}
    pull_policy: always
    container_name: app
    restart: always
    env_file:
      - /home/replace-with-your-user/broadcast/app/.env
    volumes:
      - /home/replace-with-your-user/broadcast/app/storage:/rails/storage
      - /home/replace-with-your-user/broadcast/app/uploads:/rails/uploads
    ports:
      - "127.0.0.1:3000:3000" # Overrides default ports
    networks:
      - broadcast-network
    depends_on:
      postgres:
        condition: service_healthy
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    command: bin/rails server # Overrides default command

  job:
    image: ${DOCKER_IMAGE:-gitea.hostedapp.org/broadcast/broadcast:latest}
    platform: linux/${TARGETARCH:-amd64}
    pull_policy: always
    container_name: job
    restart: always
    env_file:
      - /home/replace-with-your-user/broadcast/app/.env
    volumes:
      - /home/replace-with-your-user/broadcast/app/storage:/rails/storage
    networks:
      - broadcast-network
    depends_on:
      postgres:
        condition: service_healthy
    command:
      - "bin/jobs"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  postgres:
    image: postgres:17-alpine
    container_name: postgres
    restart: always
    env_file:
      - /home/replace-with-your-user/broadcast/db/.env
    volumes:
      - /home/replace-with-your-user/broadcast/db/backups:/backups
      - /home/replace-with-your-user/broadcast/db/postgres-data:/var/lib/postgresql/data
      - /home/replace-with-your-user/broadcast/db/init-scripts:/docker-entrypoint-initdb.d
    ports:
      - "127.0.0.1:5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U broadcast"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    networks:
      - broadcast-network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

networks:
  broadcast-network:
    driver: bridge
```

Broadcast replies on 3 containers to run:

- The app container
- The job container
- The postgres container

The names of these containers should be self explanatory.

Certain volumes need to be mounted from your host machine to the containers in order to persist data.

The app and job containers run as **uid 1000** inside the image, and they write to some of those
mounts: `app/storage`, `app/uploads`, `app/triggers`, and `ssl` (where Thruster keeps its TLS
certificate). On the host, those directories must be owned by uid 1000 or the containers cannot
write them, and the failures are quiet: uploads error, the dashboard's Upgrade button does nothing,
and no certificate is stored. The automatic installer handles this for you; with a manual install
it is your job:

```bash
cd /home/your-user/broadcast
mkdir -p app/storage app/uploads app/triggers app/monitor ssl
chown -R 1000:1000 app/storage app/uploads app/triggers ssl
```

Leave `app/monitor` owned by whichever host user writes host metrics into it, if you use that
feature; the container only reads it. Note that if a user on your host already has uid 1000
(often the default cloud user, such as `ubuntu`), that user can now write to these directories.
That user almost always has sudo anyway, so in practice nothing changes, but it is worth knowing.

### 3. Create the .env files

You will need to create the `.env` files for the app/job and db containers.

The app and db containers share the same `.env` file, which is located in the `/home/your-user/broadcast/app` directory.

Copy the sample files from `/home/your-user/broadcast/manual` to the respectively app and db directories.

There is some duplicate in the both the app and db `.env` files, so be careful to copy the correct values.

You will need to generate a `SECRET_KEY_BASE` value for the app container.

To do this, run the following command in the `/home/your-user/broadcast/app` directory:

```bash
openssl rand -hex 64
```

The value that is generated can be used for the `SECRET_KEY_BASE` variable in the `.env` file.

Next, you will need to generate a `POSTGRES_PASSWORD` value.

To do this, run the following command:

```bash
openssl rand -hex 32
```

Copy this value into both the app and db `.env` files. Note that in the app `.env` file, the password variable is called `DATABASE_PASSWORD`, and that in the db `.env` file, the password variable is called `POSTGRES_PASSWORD`.

Next, set the `TLS_DOMAIN` and `LICENSE_KEY` values to the domain name of your server and the [license key you got from the customer dashboard](https://sendbroadcast.net/docs/license).

### 4. Run the docker compose file

Before you can run the docker compose file, you will need to ensure that Docker is already installed on your server. As well, you will need to be logged into Broadcast's private Docker registry:

```bash
docker login gitea.hostedapp.org
```

Please email us to get access to the private Docker registry as you will need a username and password to log in.

Test running the Broadcast containers together with the following command:

```bash
docker compose -f docker-compose.manual.yml up
```

This should boot up the three containers and start the Broadcast app.

### 5. Setup monitored services

Once you have confirmed that everything is working correctly, press CTRL+C to stop the containers.

Next, you will need to setup monitored services for the containers.

If you are on a Linux system, you want to use systemd to manage running the containers.

For example, if you're using systemd, you can create a new service file for the Broadcast containers.

```
[Unit]
Description=Broadcast
Requires=docker.service
After=docker.service

[Service]
Type=simple
ExecStart=/bin/bash -c "docker compose -f /home/your-user/broadcast/docker-compose.yml up"
ExecStop=/bin/bash -c "docker compose -f /home/your-user/broadcast/docker-compose.yml down"
Restart=always
User=<your-user>
WorkingDirectory=/opt/broadcast

[Install]
WantedBy=multi-user.target
```

## Single-container deployment (alternative)

For users on small servers with limited resources, you can run Broadcast as a single container instead of separating the app and job containers. This runs Solid Queue (the background job processor) inside the Puma web server process.

### Configuration

To enable single-container mode, add the following environment variable to your app `.env` file:

```bash
SOLID_QUEUE_IN_PUMA=true
```

Then modify your `docker-compose.manual.yml` to remove the `job` service entirely, leaving only the `app` and `postgres` services.

### Async mode for lower memory usage

By default, Solid Queue runs in fork mode, which spawns separate processes for workers and dispatchers. On small servers, this can use significant memory.

To reduce memory usage, you can enable async mode, which runs workers in threads within the same process as Puma:

```bash
SOLID_QUEUE_IN_PUMA=true
SOLID_QUEUE_SUPERVISOR_MODE=async
```

This configuration is ideal for low-traffic deployments on servers with limited RAM.

### Trade-offs

| Mode | Pros | Cons |
|------|------|------|
| Two containers (default) | Better isolation, easier scaling, recommended for production | Uses more resources |
| Single container (fork) | Simpler deployment, fewer containers | Still uses multiple processes |
| Single container (async) | Lowest memory usage | All work in one process, less isolation |

For most production deployments, the two-container setup is recommended. Single-container mode is best suited for personal projects or low-traffic sites where simplicity and resource savings are priorities.
