Containers Up!
Containers Up! is an open source web-based container management platform designed to simplify the administration of containers across multiple remote hosts.
It provides a unified interface for managing containerized applications, and automating updates with minimal manual intervention.
This guide will step through setting up your own instance of Containers Up! and configuring Dependabot on GitHub or Renovate Bot with Forgejo.
Video
Watch on YouTubeInstallation
Prerequisites
Before getting started, you'll need:
- A server or mini PC with Docker installed
- SSH access to your server
- (Optional) A domain name with HTTPS configured via Traefik or similar reverse proxy (deSEC is a good way to test HTTPS with free subdomain and wildcard certificates)
- (Optional) A Git repository (GitHub or Forgejo) for automated updates
- (Optional) A VPS or tunnelling solution to expose your webhook port securely (AWS & Oracle Cloud both have free VPS tiers to test this setup)
Basic Setup
The simplest way to get started is with the following compose.yml:
services:
containers-up:
# https://github.com/DigitallyRefined/containers-up/releases
image: ghcr.io/digitallyrefined/containers-up:1.4.3
restart: unless-stopped
ports:
- 3000:3000 # Main dashboard
- 3001:3001 # Webhook port
volumes:
- ./storage:/storage
- ./storage/.ssh:/root/.ssh
- ./storage/.docker:/root/.docker
See the Containers Up! GitHub repository for the latest release tag.
- Create a directory for Containers Up!:
mkdir containers-up && cd containers-up - Save the above compose file
- Start the service:
docker compose up -d - Open
http://localhost:3000in your browser
Adding Your First Host
Creating a separate SSH Key for the app to use
Ideally you should create an SSH key pair specifically for Containers Up! to use:
ssh-keygen -t ed25519 -C "containers-up" -f ~/.ssh/containers-up
Then add the public key to your server's ~/.ssh/authorized_keys:
cat ~/.ssh/containers-up.pub | ssh user@yourhost 'cat >> ~/.ssh/authorized_keys'
Adding the Host in Containers Up!
When you first access the dashboard, you'll need to add a host:
-
Click Add Host in the UI
-
Fill in the following required fields:
- Name: A unique identifier (e.g.,
my-server) - lowercase letters, numbers, and hyphens only - SSH Host: Your server's SSH connection string in the format
user@hostnameoruser@ip-address - SSH Private Key: Paste your SSH private key (must be in OpenSSH format)
- Name: A unique identifier (e.g.,
-
Optional fields for automation:
- Working Folder: Path to your compose files on the remote host (e.g.,
/home/user/stacks) - Repository Host: Git provider URL (default:
https://github.com) - Repository: Your repo in format
username/repo - Bot Type: Choose
dependabot(GitHub) orrenovate(Forgejo) - Webhook Secret: Generate a random secret for webhook authentication
- Exclude Folders: Regex pattern to exclude certain folders (e.g.,
(manual|test)) - Cron Schedule: When to check for image updates (e.g.,
0 1 * * 6for Saturdays at 1 AM) - Squash Updates: Enable to automatically combine multiple dependency update commits
- Working Folder: Path to your compose files on the remote host (e.g.,
-
Click Save - the system will validate your SSH connection and Docker availability
Once added, you'll see a dashboard showing:
- Composed Containers: Services managed via compose files
- Individual Containers: Standalone containers
- Images: All Docker images on the host
- Unused Images: Images that can be cleaned up
Setup with HTTPS & Authentication (optional, but recommended!)
For production deployments with secure HTTPS access and authentication, you can use Traefik as a reverse proxy with Pocket ID for OpenID Connect authentication.
Traefik Certificate Resolver Configuration
Before deploying, you'll need to configure Traefik's certificate resolver for automatic HTTPS certificates. Create or update your Traefik configuration file at ./traefik/config/traefik.yml:
Setting up deSEC DNS Challenge:
This example uses deSEC for a free subdomain and DNS challenge to obtain wildcard certificates, but you can also use other providers such as Cloudflare.
If using the production-desec-dns resolver, you'll need to provide your deSEC API token to Traefik:
-
Sign up for a free account at deSEC.io and create a domain
-
Generate an API token in your deSEC dashboard
-
Create a
.envfile in the same directory as yourcompose.yml:# deSEC API Token for Traefik DNS Challenge
DESEC_TOKEN=your_desec_api_token_goes_here -
Update your Traefik service in
compose.ymlto use the.envfile:traefik:
image: traefik:v...
# ... other settings ...
env_file:
- ./.env -
Make sure to add
.envto your.gitignoreto avoid committing sensitive tokens
Click to expand Traefik certificate resolver configuration
certificatesResolvers:
# Staging environment (for testing)
staging-desec-dns:
acme:
dnsChallenge:
provider: desec
propagation:
delayBeforeChecks: 240
email: you@example.com
storage: /certs/acme-letsencrypt-staging-desec-dns.json
caServer: 'https://acme-staging-v02.api.letsencrypt.org/directory'
# Production (after making sure staging works, as Let's Encrypt rate limits failed attempts/restarts)
production-desec-dns:
acme:
dnsChallenge:
provider: desec
propagation:
delayBeforeChecks: 240
email: you@example.com
storage: /certs/acme-letsencrypt-production-desec-dns.json
Complete Setup Example
Click to expand complete compose.yml with all three services
services:
containers-up:
# https://github.com/DigitallyRefined/containers-up/releases
image: ghcr.io/digitallyrefined/containers-up:1.4.3
restart: unless-stopped
volumes:
- ./containers-up/storage:/storage
- ./containers-up/storage/.ssh:/root/.ssh
- ./containers-up/storage/.docker:/root/.docker
env_file:
- ./.env # Create this file based on .env.default
networks:
- traefik
labels:
traefik.enable: true
# Main dashboard (requires authentication)
traefik.http.routers.containers-up.entrypoints: websecure
traefik.http.routers.containers-up.rule: Host(`containers-up.example.dedyn.io`)
traefik.http.routers.containers-up.tls: true
traefik.http.routers.containers-up.tls.certresolver: production-desec-dns
traefik.http.routers.containers-up.service: containers-up
traefik.http.services.containers-up.loadbalancer.server.port: 3000
# Webhook endpoint (public access for GitHub/Forgejo webhooks)
traefik.http.routers.containers-up-webhook.entrypoints: websecure
traefik.http.routers.containers-up-webhook.rule: Host(`containers-up.example.dedyn.io`) && PathPrefix(`/api/webhook`)
traefik.http.routers.containers-up-webhook.tls: true
traefik.http.routers.containers-up-webhook.tls.certresolver: production-desec-dns
traefik.http.routers.containers-up-webhook.service: containers-up-webhook
traefik.http.services.containers-up-webhook.loadbalancer.server.port: 3001
pocket-id:
# https://github.com/pocket-id/pocket-id/releases
image: ghcr.io/pocket-id/pocket-id:v2.2.0
restart: unless-stopped
volumes:
- './pocket-id/data:/app/data'
environment:
- APP_URL=https://id.example.dedyn.io
- TRUST_PROXY=true
- ENCRYPTION_KEY="run `openssl rand -base64 32`" # Generate a unique key
networks:
- 'traefik'
labels:
traefik.enable: true
traefik.http.routers.pocketid.entrypoints: websecure
traefik.http.routers.pocketid.rule: Host(`id.example.dedyn.io`)
traefik.http.routers.pocketid.tls: true
traefik.http.routers.pocketid.tls.certresolver: production-desec-dns
traefik:
# https://github.com/traefik/traefik/releases
image: traefik:v3.6.7
container_name: 'traefik'
restart: unless-stopped
ports:
- '80:80'
- '443:443'
environment:
- LEGO_DISABLE_CNAME_SUPPORT=true
volumes:
- ./traefik/config:/etc/traefik
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- 'traefik'
networks:
traefik:
external: true
It can take take around 5 minutes for certificates to be issued, keep the service running monitoring docker compose logs -f and check in the certs folder, as there will be a new section added to the JSON files under PrivateKey with a Certificates section once issued.
When first starting Traefik, it's recommended to use the staging-desec-dns resolver to avoid hitting Let's Encrypt rate limits while testing your setup. Once confirmed working, switch to production-desec-dns.
Notes:
- Replace
you@example.comwith your actual email address - The
production-desec-dnsresolver uses DNS challenge with deSEC provider (free DNS service with API support) - DNS challenge is recommended for wildcard certificates or when port 80 is not accessible
- The
storagepath must match a volume mounted in your Traefik container (e.g.,./traefik/certs:/certs)
See the Traefik ACME documentation for more certificate resolver options.
Setup Steps
-
Update domains in the compose file:
- Replace
containers-up.example.dedyn.iowith your Containers Up! domain - Replace
id.example.dedyn.iowith your Pocket ID domain
- Replace
-
Generate encryption key for Pocket ID:
openssl rand -base64 32 -
Create the network and start services:
docker network create traefik
docker compose up -d -
Configure Pocket ID:
- Navigate to your Pocket ID instance (
https://id.example.dedyn.io) - Follow the Pocket ID setup guide to create an admin user
- Optionally create an admin group and add your user to it
- Navigate to your Pocket ID instance (
-
Create OIDC Client in Pocket ID:
- In Pocket ID admin, create a new OIDC client
- Set the callback URL to:
https://containers-up.example.dedyn.io/auth-callback - Optionally restrict access to the admin group only
- Copy the Client ID and Client Secret
-
Configure Containers Up! OIDC:
Create a
.envfile with the following OIDC configuration:# OpenID Connect Authentication
ENV_PUBLIC_OIDC_ISSUER_URI=https://id.example.dedyn.io
ENV_PUBLIC_OIDC_CLIENT_ID=your_client_id_here
OIDC_CLIENT_SECRET=your_client_secret_here
# OIDC_JWKS_URL=https://id.example.dedyn.io/.well-known/jwks.json # Optional: only needed if auto-discovery fails -
Restart Containers Up!:
docker compose restart containers-up -
Test authentication:
- Navigate to
https://containers-up.example.dedyn.io - You should be redirected to Pocket ID for login
- After successful login, you'll be redirected back to Containers Up!
- Navigate to
Notes
- The main dashboard (port 3000) is protected by OIDC authentication
- The webhook endpoint (port 3001) remains publicly accessible but is secured by webhook signature verification
- Make sure your
.envfile is not committed to Git (add it to.gitignore) - See the Pocket ID documentation for advanced configuration options
Environment Variables
Optional configuration such as authentication and notifications can be set via a .env file:
services:
containers-up:
image: ghcr.io/digitallyrefined/containers-up:1.4.3
# ... other settings ...
env_file:
- ./.env
See the full list in the repository's .env.default file.
Setting Up Automated Updates
One of Containers Up!'s most powerful features is automated container updates via Git-based workflows. This section covers setting up Dependabot (GitHub) or Renovate (Forgejo) to automatically create pull requests when new container images are available.
Prerequisites for Automation
Before configuring automated updates:
-
Your Containers Up! instance must be publicly accessible via HTTPS on port
3001(webhook port)- You can use Cloudflare Tunnels, Pangolin, Docker WireGuard Tunnel, or see Awesome Tunnelling for even more options
-
Create a Git repository with your
compose.ymlordocker-compose.ymlfiles -
Pin all image versions - replace
:latesttags with specific versions:# ❌ Don't use this
image: traefik
# ❌ Or this
image: traefik:latest
# ✅ Use specific versions
image: traefik:v3.6.7
Option A: GitHub with Dependabot
1. Enable GitHub Actions
In your repository Settings > Actions > General:
- Enable Allow all actions and reusable workflows
- Under Workflow permissions, allow Read and write permissions
- Allow GitHub Actions to create and approve pull requests
2. Create Dependabot Template
Create .github/dependabot.template.yml:
version: 2
enable-beta-ecosystems: true # Remove once docker-compose updates become stable
updates:
- package-ecosystem: 'docker-compose'
directory: '**/docker-compose.yml'
schedule:
interval: 'weekly'
day: 'saturday'
time: '01:23'
timezone: 'Europe/London' # <-- Change to your timezone
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
day: 'saturday'
time: '01:23'
timezone: 'Europe/London' # <-- Change to your timezone
3. Add GitHub Action to Generate Dependabot Config
Create .github/workflows/generate_dependabot.yml:
name: Generate dependabot.yml
on:
push:
branches:
- main
repository_dispatch:
workflow_dispatch:
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Generate dependabot.yml
uses: Makeshift/generate-dependabot-glob-action@master
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
This workflow will automatically create a managed .github/dependabot.yml file that updates as you add new compose files.
4. Configure Webhook in Containers Up!
In the Containers Up! dashboard:
- Edit your host configuration
- Set the Working Folder (e.g.,
/home/user/stacks) - Set Repository to
username/repo - Generate a random Webhook Secret (e.g., using
openssl rand -hex 32) - Click the ℹ️ icon to view your webhook URL (will look like:
https://containers-up.example.dedyn.io/api/webhook/github/host/YOUR_HOST) - Save the host configuration
5. Configure GitHub Webhook
In your GitHub repository Settings > Webhooks > Add webhook:
- Payload URL: Your webhook URL from step 4
- Content type:
application/json - Secret: The same webhook secret from Containers Up!
- Events: Select Let me select individual events and choose Pull requests
- Click Add webhook
Test it by clicking the webhook and selecting Recent Deliveries > Redeliver. You should see a "ping" event in the Containers Up! logs.
Option B: Forgejo with Renovate Bot
1. Create Access Tokens
In Forgejo Settings > Application:
- Create an Access Token with the permissions listed in the Renovate Forgejo docs
- In Actions > Secrets:
- Create
ACTIONS_TOKENwith your Forgejo token - Create
EXTERNAL_GITHUB_TOKENwith a GitHub Personal Access Token (for fetching changelogs)
- Create
2. Create Renovate Workflow
Click to expand .forgejo/workflows/renovate.yml
name: Renovate
on:
push:
branches:
- main
- 'renovate/**'
schedule:
# At 02:00, only on Saturday
- cron: '0 2 * * 6'
issues:
types:
- edited
workflow_dispatch: # Allow manual trigger
jobs:
renovate:
runs-on: docker
container:
image: renovate/renovate:43.0.6
steps:
- name: Restore Renovate Cache
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/renovate-cache
key: renovate-cache-${{ runner.os }}
restore-keys: |
renovate-cache-
- name: Set Git identity
run: |
git config --global user.name "Renovate Bot"
git config --global user.email "renovate@localhost"
- name: Run Renovate
env:
LOG_LEVEL: info
RENOVATE_PLATFORM: forgejo
RENOVATE_ENDPOINT: ${{ github.api_url }} # GitHub variables still work in Forgejo
RENOVATE_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
RENOVATE_REPOSITORIES: ${{ github.repository }}
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.EXTERNAL_GITHUB_TOKEN }}
RENOVATE_CACHE_DIR: ${{ github.workspace }}/renovate-cache
run: renovate
3. Configure Webhook in Containers Up!
Similar to GitHub setup:
- Edit your host, set Bot Type to
renovate - Set Working Folder, Repository (
username/repo) - Set Repository Host to your Forgejo URL (e.g.,
https://git.example.dedyn.io) - Generate a Webhook Secret
- Note the webhook URL (will include
/forgejo/in the path)
4. Configure Forgejo Webhook
In your Forgejo repository Settings > Webhooks > Add Forgejo webhook:
- Target URL: Your webhook URL
- HTTP Method:
POST - Content Type:
application/json - Secret: Your webhook secret
- Trigger On: Custom events > Pull request modifications
How It Works
Once configured, the workflow is:
- Dependabot/Renovate scans your compose files weekly (or on your schedule)
- When a new image version is found, a Pull Request is created
- The PR triggers a webhook to Containers Up!
- Containers Up! displays the update in the dashboard with a notification
- You review the changelog and decide whether to merge
- When merged, another webhook triggers Containers Up! to:
- Pull the updated compose file from Git
- Pull the new Docker image
- Restart the containers with the new image
Optional: Commit Squashing
If you enable Squash Updates in your host configuration, Containers Up! will automatically combine multiple dependency update commits to keep your Git history clean. This is useful when multiple PRs are merged in quick succession.
The squashing behavior can be customised with environment variables:
SQUASH_UPDATE_MESSAGE: Commit message prefix (default:Update dependencies)SQUASH_DAYS_AGO: Number of days before considering a commit too old to squash with newer dependency updates (default:5days)SQUASH_MAX_UPDATE_COMMITS: Maximum number of dependency update commits to keep before squashing the oldest two together (default:5)