Docker SSH Provider

Deploy preview environments to any server you can SSH into — bare metal, VPS, AWS EC2, GCP Compute Engine, etc.

How it works

Prerequisites

  1. A Linux or Unix server (bare metal, VPS, or cloud VM). The provider runs Linux shell commands (git, docker compose, base64, etc.) over SSH and is not compatible with Windows SSH servers.
  2. A server with Docker, Docker Compose v2, and Git installed.
  3. An SSH key pair with access to that server.

Step 1 — Prepare your server

# Install Docker (Ubuntu/Debian)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

# Install Git
sudo apt-get install -y git

The SSH user must be in the docker group (or be root).

Tip for small instances (1 GB RAM or less): Add swap before your first deploy — docker compose build can exhaust memory on t2.micro / e2-micro class VMs and cause the instance to freeze.

sudo fallocate -l 1G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Step 2 — Set up SSH key access

If you need a new key pair:

ssh-keygen -t ed25519 -C "previewops" -f ~/.ssh/previewops-ssh

Add the public key to the server:

ssh-copy-id -i ~/.ssh/previewops-ssh.pub user@your-server

Store the private key as SSH_PRIVATE_KEY in Previewops secrets:

cat ~/.ssh/previewops-ssh

Step 3 — Store secrets in Previewops

Go to the Previewops dashboard → your installation → Credentials, select provider docker-ssh, and add the following keys.

Minimum required (no .previewops.yaml needed):

Key Value
SSH_PRIVATE_KEY PEM-encoded private key from step 2
DOCKER_SSH_HOST IP address or hostname of your server

Optional credential keys (can also be set in .previewops.yaml — YAML takes precedence):

Key Default Description
DOCKER_SSH_USER ubuntu SSH username
DOCKER_SSH_PORT 22 SSH port
DOCKER_SSH_BASE_DIR /srv/previews Base directory on the server for per-PR folders
DOCKER_SSH_COMPOSE_FILE docker-compose.yml Docker Compose file to use
DOCKER_SSH_BASE_URL http://{host} Publicly accessible URL posted to the PR
DOCKER_SSH_HOST_KEY_FINGERPRINT SHA256 fingerprint of the server's ED25519 host key (recommended — prevents MITM). Server must have ED25519 host key support enabled (default on all modern Linux distros).

Any other credential key you store is automatically injected as an environment variable into your containers at deploy time. For example, store DATABASE_URL on the credentials page and it will be written to the .env file on the server and available inside every container. Values from .previewops.yaml env: take precedence over credentials when the same key appears in both.

Getting the fingerprint: Run this on your server:

ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

Copy the SHA256:... value into the credential field.

Step 4 — Configure the repo (optional)

If all config is stored as credentials above, you can skip the .previewops.yaml entirely for the provider. You only need it to override specific values or to set non-provider options (ttlHours, env, concurrency, etc.).

# .previewops.yaml — only needed if you want to override credential values
provider: docker-ssh
providerConfig:
  host: 203.0.113.42               # overrides DOCKER_SSH_HOST credential
  user: ubuntu                     # overrides DOCKER_SSH_USER credential (default: ubuntu)
  port: 22                         # overrides DOCKER_SSH_PORT credential (default: 22)
  baseDir: /srv/previews           # overrides DOCKER_SSH_BASE_DIR credential (default: /srv/previews)
  composeFile: docker-compose.yml  # overrides DOCKER_SSH_COMPOSE_FILE credential (default: docker-compose.yml)
  baseUrl: http://203.0.113.42     # overrides DOCKER_SSH_BASE_URL credential (default: http://{host})
                                   # include a port if needed, e.g. http://203.0.113.42:3000

Complete example with all options:

# .previewops.yaml
provider: docker-ssh
providerConfig:
  host: 203.0.113.42               # SSH server IP or hostname (required)
  user: ubuntu                     # SSH username (default: ubuntu)
  port: 22                         # SSH port (default: 22)
  baseDir: /srv/previews           # base directory on the server for all previews (default: /srv/previews)
  composeFile: docker-compose.yml  # Docker Compose file name (default: docker-compose.yml)
  baseUrl: http://203.0.113.42     # URL posted to the PR (default: http://{host})

# Common options — all optional
concurrency: 3              # max simultaneous active previews per repo (cap server load)
ttlHours: 24                # auto-delete after N hours (default: 24)
port: 8080                  # port your compose service exposes on the host (default: 8080)
dockerfile: Dockerfile      # Dockerfile path relative to repo root (default: Dockerfile)
buildContext: .             # Docker build context (default: .)
env:
  NODE_ENV: preview         # inject env vars into the preview container

Step 5 — Update your docker-compose.yml

Unlike other providers, docker-ssh does not inject env vars into your container automatically. Other providers (Fly, Render, Railway, etc.) manage the container runtime directly via platform APIs and can inject secrets without any configuration. With docker-ssh, your docker-compose.yml controls the container — Previewops can only write a .env file to disk on the server.

Previewops writes a .env file to the PR working directory before each deploy. This file contains:

To pass these into your containers, add env_file: .env to each service in your docker-compose.yml:

services:
  app:
    build: .
    ports:
      - "${PORT:-3000}:3000"   # PORT substituted into the YAML (this already works)
    env_file: .env             # ← required: passes DATABASE_URL, secrets, etc. into the container

Without env_file: .env, Previewops writes the file to disk but the variables never reach the container's process environment. ${PORT} in the port mapping works because compose resolves YAML template variables from the file automatically — that is a separate mechanism from injecting them into the container.

Step 6 — Verify

Comment /validate-previewops on any open PR. The bot opens an SSH connection, runs docker info, and reports success or failure.

Notes

Using GCP free-tier Compute Engine

The e2-micro instance is always free in us-west1, us-central1, or us-east1:

gcloud compute instances create preview-server \
  --machine-type=e2-micro \
  --zone=us-central1-a \
  --image-family=ubuntu-2404-lts-amd64 \
  --image-project=ubuntu-os-cloud \
  --tags=http-server

# Allow HTTP traffic
gcloud compute firewall-rules create allow-preview-ports \
  --allow=tcp:1024-65535 \
  --target-tags=http-server

Then follow steps 1–4 using the instance's external IP as host.

Troubleshooting

Error Fix
SSH_PRIVATE_KEY env var is required Set SSH_PRIVATE_KEY in credentials
docker-ssh requires a host Set DOCKER_SSH_HOST in credentials or providerConfig.host in .previewops.yaml
Connection refused Check the server firewall / security group allows port 22
Permission denied (publickey) The public key isn't in ~/.ssh/authorized_keys on the server
docker: command not found Docker isn't installed or the SSH user isn't in the docker group
mkdir: cannot create directory '/srv/previews': Permission denied The SSH user doesn't have write access to the base directory. Run sudo mkdir -p /srv/previews && sudo chown $USER:$USER /srv/previews on the server, or set DOCKER_SSH_BASE_DIR to a directory the user owns (e.g. /home/ubuntu/previews).
Container exits immediately Use docker logs <container-name> on the server. docker ps -a shows stopped containers. docker compose logs without --project-name may return nothing — use docker logs directly.
DATABASE_URL or other secrets not available in the container Add env_file: .env to each service in docker-compose.yml. Previewops writes the .env file to disk but compose does not inject it into the container's environment unless you declare env_file. Also check that environment: in your compose file doesn't override the same key — explicit environment: values always win over env_file.
SSH host key fingerprint mismatch The stored fingerprint doesn't match the server's ED25519 key. Re-run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the server and update the DOCKER_SSH_HOST_KEY_FINGERPRINT credential. Ensure you are using the ED25519 key, not RSA or ECDSA.