[Blog](https://deploywithox.com/blog) [Guides](https://deploywithox.com/blog#c-guides) # How to self-host wallabag on your own VPS, without Docker Run wallabag 2.6.14, the read-it-later app, on your own Ubuntu server with ox: PHP, PostgreSQL and a first admin made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 12 min read On this page - [What you need](https://deploywithox.com/blog/self-host-wallabag#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-wallabag#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-wallabag#config) - [The database and what is not here](https://deploywithox.com/blog/self-host-wallabag#services) - [Updates and backups](https://deploywithox.com/blog/self-host-wallabag#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-wallabag#faq) Self-hosting wallabag takes an Ubuntu server, your own fork of the wallabag repo that carries an `ox.toml` and three PHP files, and seven variables. In our run the first deploy took about 2 minutes, and ox set up the PostgreSQL database for it. **How we tested** On Oct 17, 2026, ox deployed wallabag 2.6.14 from commit [`74cbfd94`](https://github.com/wallabag/wallabag/commit/74cbfd945bc316b5cf0308ba56e32fd2ed3443c0) on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, saved an article from its link and saw it in the Unread list with its reading time after a reload, and checked the app's services and its backup. [wallabag](https://github.com/wallabag/wallabag) is a web app for saving web pages to read later. You paste a link, wallabag fetches the page and keeps the article text without pop-ups, and you read it from a list of unread entries. It is a PHP app built on Symfony, under the MIT license, with a browser extension and Android and iOS apps from the project. ox deployed wallabag 2.6.14 (commit 74cbfd94) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 2 minutes and a redeploy took 33 seconds. - **First deploy:** 2 minutes, from adding the project until the app passed the run's first checks. - **Redeploy of the same commit:** 33 seconds, until the app passed the checks again. - **Server:** DigitalOcean, 8 vCPUs and 16 GB of memory (`s-8vcpu-16gb`), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app. - **Memory in use after the reboot:** 769 MB of 15986 MB, for the whole server: Ubuntu, the app, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less. - **Services ox runs for it:** `postgres`. - **Source:** [github.com/wallabag/wallabag](https://github.com/wallabag/wallabag) at 2.6.14, commit [`74cbfd94`](https://github.com/wallabag/wallabag/commit/74cbfd945bc316b5cf0308ba56e32fd2ed3443c0). The wallabag log-in page The wallabag quickstart page after logging in Two saved Wikipedia articles in wallabag's unread list ## What you need - An Ubuntu LTS server. The run used the size in the facts above and measured the memory there. wallabag's `composer.json` asks for PHP 7.4 or newer with several extensions, among them intl, gd and tidy. The config adds one system package, `php-pgsql`. - A GitHub account to fork `wallabag/wallabag`, and an ox account, which you open by signing in with GitHub. - A user name, a password and an email address, such as `you@example.com`, for the first admin. - Outbound internet from the server, which downloads each page you save. Email was not tested: the run pointed wallabag at a test mail catcher. ## The steps On the ox dashboard choose Add a server and copy the install command it prints. Run it as root on your Ubuntu machine. It installs the ox agent and Caddy. Fork `wallabag/wallabag` and start from the tag `2.6.14`, commit `74cbfd94`, the release the run deployed. Add `ox.toml`, `ox/install.php`, `ox/parameters.php` and `web/index.php` at the paths shown below, then commit and push. On your laptop write `wallabag.env`, one `KEY=value` line for each key in the variables section. wallabag's example settings file asks for a secret that is random, and `APP_SECRET` fills that setting. With the [ox CLI](https://deploywithox.com/docs/cli) signed in, run: Shell ```bash ox new /wallabag ox review wallabag --from-file wallabag.env --wait ``` The project takes its name from the repo. The review saves your values and starts the first deploy, and `--wait` follows the log to its end. The [quickstart](https://deploywithox.com/docs/quickstart) shows the browser route. When the deploy is green, open the project's address. It sends you to `/login`, where a wrong password answers "Invalid credentials." Sign in with `WALLABAG_ADMIN_USER` and `WALLABAG_ADMIN_PASSWORD` and wallabag opens its Quickstart page. Press the plus sign in the top bar, paste an article's address and press Enter. After a reload the Unread list shows the article with its title, site and reading time. The run saved a Wikipedia article this way, and it read 15 min. The run served wallabag on its preview address only. A domain of your own was not part of it, and the [domains page](https://deploywithox.com/docs/operate/domains) covers how ox adds one. ## The ox.toml, line by line TOML ```toml # wallabag 2.6 (Symfony 4.4 before Flex) on PostgreSQL, set up the way its # install guide does it: composer install, the migrations, then # wallabag:install once. ox's detected start serves web/ with FrankenPHP, # whose index.php (web/index.php here) trusts ox's proxy and runs app.php. # The console's PostgreSQL driver; FrankenPHP has its own. packages = ["php-pgsql"] # The first admin, made by wallabag:install (ox/install.php) in migrate. required_variables = ["WALLABAG_ADMIN_USER", "WALLABAG_ADMIN_PASSWORD", "WALLABAG_ADMIN_EMAIL"] [app] health = "/login" # wallabag writes its cache folder while it runs, beyond the pools folder # ox makes writable: the site settings cache (craue_config) and the article # fetcher's cookie jar (var/cache/prod/cookiejar.json). sandbox = "relaxed" [build] # ox/parameters.php writes app/config/parameters.yml from the variables # first: composer's post-install scripts boot the app. install = "php ox/parameters.php && SYMFONY_ENV=prod composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader" # The release's index.php is the overlay's, so no link to app.php here. commands = ["php bin/console cache:warmup --env=prod --no-debug"] migrate = ''' php bin/console doctrine:migrations:migrate --env=prod --no-debug --no-interaction --allow-no-migration php ox/install.php ''' [services] postgres = {} [storage] # data: the key that encrypts saved site credentials; web/assets/images: # images saved with articles; web/uploads/import: files being imported; # var/sessions: sign-ins, so a deploy does not sign everyone out. keep = ["data", "web/assets/images", "web/uploads/import", "var/sessions"] ``` wallabag 2.6 runs on Symfony 4.4, which predates Symfony's Flex layout. Its front controller is `web/app.php`, and ox serves the `web/` folder through FrankenPHP. - `packages` adds the PostgreSQL driver for PHP's command line, where the migrations and the installer run. FrankenPHP has its own. - `required_variables` names the three admin keys. wallabag itself never reads them. `ox/install.php` does, and listing them makes the review ask for them before anything builds. - `health` is `/login`, which answers without a session. ox waits for it before moving traffic to a new release. - `sandbox = "relaxed"` is there because the default lets a PHP app write only its data folder and tool caches. wallabag also writes inside `var/cache/prod` while it runs: its settings cache and its article fetcher's cookie jar. Relaxed lets it write anywhere in the project's home. - `install` runs `ox/parameters.php` first, because Composer's post-install scripts boot the app and it needs its settings file by then. Then comes `composer install` for production, without dev packages. - The one build command warms the production cache. It makes no link from `web/index.php` to `app.php`, because the repo gets its own `web/index.php`. - `migrate` runs wallabag's Doctrine migrations, then `ox/install.php`. With `--allow-no-migration`, a deploy that has nothing to migrate is not an error. - `postgres = {}` asks for a PostgreSQL database and a role of its own, with its address in `DATABASE_URL`. - `keep` lists four folders. `data` holds the key that encrypts the logins you save for sites, `web/assets/images` holds pictures saved with articles, `web/uploads/import` holds files waiting to be imported, and `var/sessions` holds sign-ins, so a deploy does not log everyone out. `ox/install.php` Code ``` boot(); // The counts go through the connection and table names taken now: the // installer's cache:clear replaces the cache folder, so the container's // lazy services cannot load after it (getUserRepositoryService.php). $em = $kernel->getContainer()->get('doctrine')->getManager(); $db = $em->getConnection(); $tables = [ $em->getClassMetadata(InternalSetting::class)->getTableName(), $em->getClassMetadata(User::class)->getTableName(), ]; function counts(Connection $db, array $tables): array { return array_map(fn (string $t): int => (int) $db->fetchOne('SELECT COUNT(*) FROM '.$db->quoteIdentifier($t)), $tables); } [$settings, $users] = counts($db, $tables); if ($users > 0) { echo "ox/install.php: wallabag is installed ($settings settings, $users users): nothing to do\n"; exit(0); } // The installer's questions, in its order: reset the database ox made (no), // reset the schema the migrations made (no), make an admin (yes), its name, // password and email. Each answer is read only when its question is asked, // so a "yes" can never land on a reset question. $answers = "n\nn\ny\n$username\n$password\n$email\n"; $stream = fopen('php://memory', 'r+'); fwrite($stream, $answers); rewind($stream); // Symfony's console reads answers only from an interactive input, which it // decides from a terminal or this variable. putenv('SHELL_INTERACTIVE=1'); $application = new Application($kernel); $application->setAutoExit(false); $input = new ArrayInput(['command' => 'wallabag:install', '--env' => 'prod', '--no-debug' => true]); $input->setStream($stream); $code = $application->run($input, new ConsoleOutput()); if (0 !== $code) { exit($code); } [$settings, $users] = counts($db, $tables); if (0 === $settings || 0 === $users) { fwrite(STDERR, "ox/install.php: wallabag:install did not finish: $settings settings, $users users\n"); exit(1); } echo "ox/install.php: installed, $users users\n"; ``` `ox/parameters.php` Code ``` 'pdo_pgsql', 'database_host' => $db['host'], 'database_port' => $db['port'] ?? 5432, 'database_name' => rawurldecode(ltrim($db['path'], '/')), 'database_user' => rawurldecode($db['user']), 'database_password' => rawurldecode($db['pass'] ?? ''), // wallabag's own note: PostgreSQL takes utf8, not utf8mb4. 'database_charset' => 'utf8', // The site's address: links in mails and feeds, and the asset URLs. 'domain_name' => rtrim(need('PUBLIC_URL'), '/'), 'secret' => need('APP_SECRET'), // The admin adds people under Users; an open sign-up page is off. 'fosuser_registration' => false, ]; // Mail (password reset, two-factor codes): the dist file's smtp://127.0.0.1 // unless MAILER_DSN names a server. if (false !== getenv('MAILER_DSN') && '' !== getenv('MAILER_DSN')) { $parameters['mailer_dsn'] = getenv('MAILER_DSN'); } // JSON is YAML, so each value is written as JSON. $out = "# Written by ox/parameters.php from the project's variables.\nparameters:\n"; foreach ($parameters as $key => $value) { $out .= ' '.$key.': '.json_encode(literal($value), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)."\n"; } $file = dirname(__DIR__).'/app/config/parameters.yml'; if (false === file_put_contents($file, $out)) { fwrite(STDERR, "ox/parameters.php: could not write $file\n"); exit(1); } echo "ox/parameters.php: wrote app/config/parameters.yml\n"; ``` `web/index.php` Code ```