What you end up running
You get a private automation platform on hardware you rent. The compose file in the Activepieces repository starts it as four containers:
- app serves the web interface and the API, on port 8080 of the host as shipped.
- worker runs your flows; the file starts five copies of it.
- postgres (the pgvector build of Postgres 14) holds flows, run history and your app connections.
- redis holds the job queue.
Your data sits in a Docker volume called postgres_data, not in the folder you cloned. Files that steps create are kept in the database too, unless you switch AP_FILE_STORAGE_LOCATION to S3.
How much server do you need?
Begin at the install guide's minimum of 2 vCPU with 4 GB RAM, then grow with the flows you expect to run at once. Production Setup gives per-part figures for a larger, split deployment:
| Part | Figure from the docs |
|---|---|
| A worker taking one job at a time | 0.5 vCPU and 1 GB |
| A worker taking five jobs at a time (the default) | roughly 2.5 vCPU and 5 GB |
| The app | 1 vCPU and 1 GB |
| Postgres, as a starting point | 2 vCPU and 4 GB |
| Redis | 1 vCPU and 1 GB |
The catch with the shipped file, worked out by us rather than quoted: five workers at five jobs each means 25 flows at once and, by the figure above, around 25 GB of memory. The guide itself says five replicas is too many for a small box, so lower replicas: under the worker service before you start.
For a team's automations, an everyday single-processor server or a two-processor one from the dedicated server range gives you memory to spare and nobody else's work next to yours. Every rack server and blade includes an out-of-band console: a firewall mistake that cuts off SSH still leaves you the screen and the power button; a console is also fitted to two cartridge models, the m510 and the m710x. A small cartridge with 16 GB can carry a personal setup.
A ready server reaches you within 30 minutes of the payment being approved; a custom build takes from a few hours to several working days, depending on the parts. The port is unmetered, so webhook traffic never becomes a bandwidth line on your invoice. You manage the server; we keep the hardware and the network running.
Up and running in one sitting
Install Ubuntu from the control panel, log in as root, and follow docs.docker.com to put Docker Engine on, together with its Compose plugin. Then run the block below. It follows the manual route in the Activepieces docs. Pointing the worker at http://app is one of the docs' own steps; ours are turning the firewall on first, creating .env private before any secret lands in it, and keeping the app reachable only from the server itself until you are ready. We skip one docs step deliberately, the edition setting covered in the licence section, which leaves you on the Community edition.
# Firewall first: IPv6 rules on, SSH allowed, then enable
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
# Docker Engine 28+ and the Compose v2 plugin must already be installed (docs.docker.com)
apt install -y git openssl
git clone --depth 1 https://github.com/activepieces/activepieces.git /opt/activepieces
cd /opt/activepieces
(umask 077 && sh tools/deploy.sh) # .env is created at mode 600
# Ours: app on loopback. Docs step: the worker reaches the app service
sed -i "s/'8080:80'/'127.0.0.1:8080:80'/" docker-compose.yml
sed -i '/- AP_CONTAINER_TYPE=WORKER/a\ - AP_FRONTEND_URL=http://app' docker-compose.yml
if [ "$(stat -c %a .env)" = 600 ] &&
[ "$(grep -cE '^(AP_ENCRYPTION_KEY|AP_JWT_SECRET|AP_POSTGRES_PASSWORD)=.' .env)" = 3 ] &&
grep -q "'127.0.0.1:8080:80'" docker-compose.yml &&
grep -q 'AP_FRONTEND_URL=http://app' docker-compose.yml; then
docker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d
docker compose -p activepieces -f /opt/activepieces/docker-compose.yml ps
else
echo "checks failed: nothing started"
fi
- The
ifstarts nothing unless.envis mode 600, all three secrets have values (the docs warn that withoutopensslthe script reports success with empty ones), the app is bound to 127.0.0.1 and the worker override is in place. - If it prints checks failed: nothing started, fix the false check and paste the
ifagain. Otherwisepsshould list every container as running. - The script hands each new secret to
sedas a command-line argument, so for a momentpscan show it to other users. Run it while nobody else is logged in.
Why loopback matters: a port Docker publishes without an address listens on both 0.0.0.0 and [::], and that IPv4 traffic goes around ufw entirely. Over IPv6 it slips past ufw only when Docker runs its own IPv6 networking; otherwise docker-proxy is what answers there, and ufw still applies to it. A 127.0.0.1 mapping stays strictly local from Docker Engine 28 onwards (moby#45610). Also worth knowing: anyone permitted to use docker inspect on this box can see those secrets.
Claim the admin seat before anyone else can
In the words of the install guide: "The first account you create becomes the platform administrator. There is no default username or password." You want that account to be yours. Before the proxy exists, open a tunnel from your own laptop with ssh -N -L 8080:127.0.0.1:8080 root@your.server.ip, visit http://localhost:8080 and register. That address is also the AP_FRONTEND_URL the example .env begins with, so links work during setup.
Once that account exists, strangers cannot simply join. In authentication.service.ts, signing up to an existing platform requires an invitation; setting AP_ALLOW_OPEN_SIGN_UP=true lifts that, and the 0.88.2 notes in the breaking-changes list confirm it. Leave the variable out and add colleagues with the Invite button under Platform Admin, Users.
Deciding how flow code is isolated
Code steps run in whatever sandbox AP_EXECUTION_MODE picks, and the example .env begins on UNSANDBOXED. The docs offer four modes:
UNSANDBOXED: fast, npm packages allowed in Code steps, not safe when several tenants share the platform.SANDBOX_CODE_ONLY: each Code step in a fresh V8 isolate capped at 128 MB, with norequire, no file system and no npm; safe for several tenants, no privileged container.SANDBOX_PROCESS: kernel namespaces through the isolate tool; npm works, but the worker must run privileged and every run boots cold.SANDBOX_CODE_AND_PROCESS: both layers, privileged, no npm.
For a single tenant the docs advise no sandboxing. When outsiders can build flows, choose SANDBOX_CODE_ONLY; our reasoning is that an unsandboxed step shares the worker's Docker network with Redis and Postgres. Adding AP_NETWORK_MODE=STRICT stops user code reaching private, loopback, link-local and cloud-metadata addresses, which the docs describe as in-process, best-effort protection rather than a firewall.
Going public: a domain, HTTPS and webhooks
Webhooks are why the public step matters: Activepieces builds every webhook and redirect URL from AP_FRONTEND_URL, and outside services have to reach that address, so you cannot hide the site behind an IP allow-list. Work in this order: the DNS record, then the certificate, then the new address in .env. The location section, with the upgrade headers the live editor needs, is taken from the HTTPS page of the docs. Wrapping it in a plain port-80 server for certbot to build on is our choice, as is writing 127.0.0.1 where the docs say localhost, to match the loopback binding.
# Step 1 (done first): A and AAAA records for automations.example.com point here
ufw allow 80,443/tcp
apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/activepieces <<'EOF'
server {
listen 80;
listen [::]:80;
server_name automations.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
EOF
ln -s /etc/nginx/sites-available/activepieces /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# Step 2: the certificate (certbot writes the HTTPS block and the redirect)
certbot --nginx -d automations.example.com --redirect -m you@example.com --agree-tos --no-eff-email
# Step 3: tell Activepieces its public address, then recreate app and worker
sed -i 's|^AP_FRONTEND_URL=.*|AP_FRONTEND_URL=https://automations.example.com|' /opt/activepieces/.env
docker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d --force-recreate app worker
On Ubuntu 24.04, nginx is in the main component, while certbot and its nginx plugin are in universe. Run certbot without the last three flags and it asks you, in turn, for an email address for expiry warnings, whether you agree to the subscriber terms of Let's Encrypt, and whether the EFF may have your address. If the Test Flow button does nothing afterwards, the docs point to a proxy that is not passing websockets.
Backups that restore
Keep the encryption key with every dump: the install guide warns that without AP_ENCRYPTION_KEY there is no way to decrypt your saved app connections, not even from a complete database backup. The script saves the database, .env and your compose file nightly and copies them off the machine; Redis is skipped, as the Disaster Recovery page advises.
install -d -m 700 /var/backups/activepieces
install -m 700 /dev/null /opt/activepieces/backup.sh
cat > /opt/activepieces/backup.sh <<'EOF'
set -euo pipefail
umask 077
out=/var/backups/activepieces; day=$(date +%F)
dc="docker compose -p activepieces -f /opt/activepieces/docker-compose.yml"
$dc exec -T postgres pg_dump -U postgres -Fc activepieces > $out/db-$day.dump
$dc exec -T postgres pg_restore -l < $out/db-$day.dump > /dev/null
cp /opt/activepieces/.env $out/env-$day
cp /opt/activepieces/docker-compose.yml $out/compose-$day.yml
EOF
printf '%s\n' \
'30 2 * * * root umask 077; bash /opt/activepieces/backup.sh > /var/backups/activepieces/last.log 2>&1' \
'0 3 * * * root rsync -a /var/backups/activepieces/ copies@offsite.example.org:activepieces/ > /root/ap-offsite.log 2>&1' \
> /etc/cron.d/activepieces-backup
Before the copy to offsite.example.org can run unattended, root needs a key that machine accepts. As root, run ssh-keygen -t ed25519; it asks where to save the key (press Enter) and for a passphrase twice (press Enter both times). Then ssh-copy-id copies@offsite.example.org asks you to accept the host fingerprint and to type that account's password once. Old files are not pruned for you.
To restore on the live server, work as root in /opt/activepieces (the database container is named postgres in the compose file):
- Load the dump you want and read it:
docker cp /var/backups/activepieces/db-YYYY-MM-DD.dump postgres:/tmp/wanted.dump, thendocker exec postgres pg_restore -l /tmp/wanted.dump. A list of contents means the file is sound. - Stop Activepieces but leave the database up:
docker compose -p activepieces -f /opt/activepieces/docker-compose.yml stop app worker. - Save what is there now and prove the save reads back:
docker exec postgres pg_dump -Fc -U postgres -f /tmp/before.dump activepieces,docker exec postgres pg_restore -l /tmp/before.dump, thendocker cp postgres:/tmp/before.dump /var/backups/activepieces/. If any of those fails, do not continue. - Swap the database:
docker exec postgres dropdb -U postgres activepieces,docker exec postgres createdb -U postgres activepieces, thendocker exec postgres pg_restore -d activepieces -U postgres /tmp/wanted.dump. - Confirm
.envcarries the sameAP_ENCRYPTION_KEYas the backup, rundocker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d, and sign in to check your flows. - Unhappy with the result? Repeat step 4 with
/tmp/before.dump. Remove the saved copy once you are sure.
Moving to a fresh server: copy the .env and compose file you saved into /opt/activepieces before you ever start the postgres container, since it sets its user and password from those values on its very first start. Start only the database with docker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d postgres, do steps 1 and 4, then start the rest with docker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d.
Updates, and what the licence lets you do
Your version is the image tag written into the compose file; the install guide leaves moving it to you. Each time, run the backup, read the breaking-changes page for every release you are skipping past, then set NEW to the release you picked on GitHub and run sed -i -E "s|(activepieces/activepieces:)[^ ]*|\1$NEW|" /opt/activepieces/docker-compose.yml. grep -c ":$NEW" /opt/activepieces/docker-compose.yml must answer 2 (app and worker); then run docker compose -p activepieces -f /opt/activepieces/docker-compose.yml pull followed by docker compose -p activepieces -f /opt/activepieces/docker-compose.yml up -d. If a release disappoints, the Rollback page has a migration-reversing command run from the newer image, or you restore last night's dump with the previous tag put back.
Most of Activepieces is MIT-licensed, per the repository's LICENSE file. The exceptions are packages/ee and the app/ee folder of the server API, covered by the Activepieces Enterprise licence in packages/ee/LICENSE: it lets holders of a valid Enterprise licence run that code in production, and lets anyone copy and change it for development and testing. The docs' licence page adds that the enterprise features need a licence from Activepieces on a self-hosted instance.
This is our general reading and not legal advice: MIT asks only that copies you hand on carry its licence text and copyright line; you owe nobody your source code. Installs run the Community edition unless told otherwise (system.ts). The install guide's manual steps include setting AP_EDITION=ee and switching the execution mode to SANDBOX_CODE_ONLY (ee refuses to start unsandboxed); this page leaves that step out, and a Community install has no way to take a licence key afterwards. If a purchase is possible, put both settings into .env ahead of the very first compose up.
Weighing other tools? Our n8n workload page covers that one, and a Postgres server of its own is the next step if the database outgrows this box.
Questions people ask
- Can I try it without a domain name?
- Yes. You can install, create the admin account and build flows through the SSH tunnel on
localhost:8080. The environment reference names what needs a public address: webhooks and app triggers, which outside services must be able to reach. - Does a self-hosted install send usage data to Activepieces?
- Product analytics are on by default:
AP_TELEMETRY_ENABLEDstarts astrue. You can switch Product analytics off under Platform Admin, Account, Configurations, where the docs also list the events collected. - If I move servers, do my app connections survive?
- They do if the new server gets the same
AP_ENCRYPTION_KEY, because the connections are stored encrypted with it. Restore the dump and copy the old.envacross before the database first starts, as in the restore note above.