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
- Build: SSHs into the server, clones (or pulls) the repo, and runs
docker compose build. - Deploy: runs
docker compose up -d. Each PR gets its own subdirectory underbaseDir. - Delete: runs
docker compose down --rmi localand removes the directory. - List: reads
.previewops.jsonmetadata files written to each PR directory on the server.
Prerequisites
- 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. - A server with Docker, Docker Compose v2, and Git installed.
- 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 buildcan 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_URLon the credentials page and it will be written to the.envfile on the server and available inside every container. Values from.previewops.yamlenv: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.pubCopy 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.ymlcontrols the container — Previewops can only write a.envfile to disk on the server.
Previewops writes a .env file to the PR working directory before each deploy. This file contains:
PORT— the host port the preview is bound toenv:values from.previewops.yaml- Secrets from the Secrets & Environment Variables dashboard panel
- Any non-reserved BYOC credential keys
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
- Each PR gets its own subdirectory:
{baseDir}/{owner}/{repo}/pr{number}/. - The preview URL is
{baseUrl}(defaults tohttp://{host}). If your compose service maps to a non-standard host port, you must include the port inbaseUrl— e.g.http://203.0.113.42:8080. Previewops injectsPORT=8080by default; match your port mapping andbaseUrlaccordingly. - Cost estimate in PR comments shows
N/A (your own infrastructure). - The server must have sufficient disk and memory for all concurrent previews — use
concurrencyin.previewops.yamlto cap them.
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. |