Cloudron makes it easy to run web apps like WordPress, Nextcloud, GitLab on your server. Find out more or install now.


Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Bookmarks
  • Search
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
Brand Logo

Cloudron Forum

Apps - Status | Demo | Docs | Install
J

jk

@jk
Unfollow Follow
About
Posts
54
Topics
10
Shares
0
Groups
0
Followers
0
Following
0

Posts

Recent Best Controversial

  • Support PowerDNS Provider
    J jk

    @james Any updates? Or do you know of a good way I can test it myself?

    Also, remember that you have the full patch here in message #11.

    Feature Requests

  • Support PowerDNS Provider
    J jk

    @james Did you get any further with this? Thank you.

    Feature Requests

  • Support PowerDNS Provider
    J jk

    Hi @james, I signed out and in but still can't do anything.

    Feature Requests

  • Support PowerDNS Provider
    J jk

    In place of that, this is the entire patch. You can save it and apply it with git apply <filename>.

    From 15c099056f54257130a856a9d68b511be9e090f1 Mon Sep 17 00:00:00 2001
    From: JK <redacted@redacted>
    Date: Tue, 14 Apr 2026 17:19:08 +0000
    Subject: [PATCH] Implement PowerDNS provider
    
    ---
     dashboard/public/translation/en.json          |   2 +
     .../src/components/DomainProviderForm.vue     |  11 ++
     dashboard/src/models/DomainsModel.js          |   4 +
     src/dns.js                                    |   3 +-
     src/dns/powerdns.js                           | 165 ++++++++++++++++++
     src/test/dns-providers-test.js                |  73 ++++++++
     7 files changed, 257 insertions(+), 9 deletions(-)
     create mode 100644 src/dns/powerdns.js
    
    diff --git a/dashboard/public/translation/en.json b/dashboard/public/translation/en.json
    index 4ba29505f..257ab1e22 100644
    --- a/dashboard/public/translation/en.json
    +++ b/dashboard/public/translation/en.json
    @@ -825,6 +825,8 @@
                 "domain": "Domain",
                 "provider": "DNS provider",
                 "route53AccessKeyId": "Access key ID",
    +            "powerdnsApiUrl": "PowerDNS API URL (e.g., https://ns1.example.com:8081)",
    +            "powerdnsApiKey": "API Key",
                 "route53SecretAccessKey": "Secret access key",
                 "gcdnsServiceAccountKey": "Service account key",
                 "digitalOceanToken": "DigitalOcean token",
    diff --git a/dashboard/src/components/DomainProviderForm.vue b/dashboard/src/components/DomainProviderForm.vue
    index e17fad14e..713010a3f 100644
    --- a/dashboard/src/components/DomainProviderForm.vue
    +++ b/dashboard/src/components/DomainProviderForm.vue
    @@ -56,6 +56,7 @@ function needsPort80(dnsProvider, tlsProvider) {
     function resetFields() {
       dnsConfig.value.accessKeyId = '';
       dnsConfig.value.accessKey = '';
    +  dnsConfig.value.apiUrl = '';
       dnsConfig.value.accessToken = '';
       dnsConfig.value.apiKey = '';
       dnsConfig.value.appKey = '';
    @@ -134,6 +135,16 @@ function onGcdnsFileInputChange(event) {
         <div class="warning-label" v-show="provider === 'manual'" v-html="$t('domains.domainDialog.manualInfo')"></div>
         <div class="warning-label" v-show="needsPort80(provider, tlsProvider)" v-html="$t('domains.domainDialog.letsEncryptInfo')"></div>
     
    +    <!-- powerdns -->
    +    <FormGroup v-if="provider === 'powerdns'">
    +      <label for="powerdnsApiUrlInput">{{ $t('domains.domainDialog.powerdnsApiUrl') }}</label>
    +      <TextInput id="powerdnsApiUrlInput" type="url" v-model="dnsConfig.apiUrl" placeholder="https://ns1.example.com:8081" required />
    +    </FormGroup>
    +    <FormGroup v-if="provider === 'powerdns'">
    +      <label for="powerdnsApiKeyInput">{{ $t('domains.domainDialog.powerdnsApiKey') }}</label>
    +      <MaskedInput id="powerdnsApiKeyInput" v-model="dnsConfig.apiKey" required />
    +    </FormGroup>
    +
         <!-- Route53 -->
         <FormGroup v-if="provider === 'route53'">
           <label for="accessKeyIdInput">{{ $t('domains.domainDialog.route53AccessKeyId') }}</label>
    diff --git a/dashboard/src/models/DomainsModel.js b/dashboard/src/models/DomainsModel.js
    index f3c65a70a..8a4f452cd 100644
    --- a/dashboard/src/models/DomainsModel.js
    +++ b/dashboard/src/models/DomainsModel.js
    @@ -23,6 +23,7 @@ const providers = [
       { name: 'Porkbun', value: 'porkbun' },
       { name: 'Vultr', value: 'vultr' },
       { name: 'Wildcard', value: 'wildcard' },
    +  { name: 'PowerDNS', value: 'powerdns' },
       { name: 'Manual (not recommended)', value: 'manual' },
       { name: 'No-op (only for development)', value: 'noop' }
     ];
    @@ -90,6 +91,9 @@ function filterConfigForProvider(provider, config) {
         case 'porkbun':
           props = ['apikey', 'secretapikey'];
           break;
    +    case 'powerdns':
    +      props = ['apiUrl', 'apiKey'];
    +      break;
       }
     
       const ret = {
    diff --git a/src/dns.js b/src/dns.js
    index 364f37af8..fde71cb72 100644
    --- a/src/dns.js
    +++ b/src/dns.js
    @@ -34,6 +34,7 @@ import dnsNoop from './dns/noop.js';
     import dnsManual from './dns/manual.js';
     import dnsOvh from './dns/ovh.js';
     import dnsPorkbun from './dns/porkbun.js';
    +import dnsPowerdns from './dns/powerdns.js';
     import dnsWildcard from './dns/wildcard.js';
     
     const { log } = logger('dns');
    @@ -45,7 +46,7 @@ const DNS_PROVIDERS = {
         godaddy: dnsGodaddy, inwx: dnsInwx, linode: dnsLinode, vultr: dnsVultr,
         namecom: dnsNamecom, namecheap: dnsNamecheap, netcup: dnsNetcup, hetzner: dnsHetzner,
         hetznercloud: dnsHetznercloud, noop: dnsNoop, manual: dnsManual, ovh: dnsOvh,
    -    porkbun: dnsPorkbun, wildcard: dnsWildcard
    +    porkbun: dnsPorkbun, powerdns: dnsPowerdns, wildcard: dnsWildcard
     };
     
     // choose which subdomain backend we use for test purpose we use route53
    diff --git a/src/dns/powerdns.js b/src/dns/powerdns.js
    new file mode 100644
    index 000000000..a4f072ff7
    --- /dev/null
    +++ b/src/dns/powerdns.js
    @@ -0,0 +1,165 @@
    +import assert from 'node:assert';
    +import BoxError from '../boxerror.js';
    +import logger from '../logger.js';
    +import dns from '../dns.js';
    +import safe from '@cloudron/safetydance';
    +import superagent from '@cloudron/superagent';
    +import waitForDns from './waitfordns.js';
    +
    +const { log } = logger('dns/powerdns');
    +
    +function formatError(response) {
    +    return `PowerDNS error ${response.status} ${response.body ? JSON.stringify(response.body) : response.text}`;
    +}
    +
    +function removePrivateFields(domainObject) {
    +    delete domainObject.config.apiKey;
    +    return domainObject;
    +}
    +
    +function injectPrivateFields(newConfig, currentConfig) {
    +    if (!Object.hasOwn(newConfig, 'apiKey')) newConfig.apiKey = currentConfig.apiKey;
    +}
    +
    +async function get(domainObject, subdomain, type) {
    +    assert.strictEqual(typeof domainObject, 'object');
    +    assert.strictEqual(typeof subdomain, 'string');
    +    assert.strictEqual(typeof type, 'string');
    +
    +    const domainConfig = domainObject.config;
    +    const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
    +    const zoneName = domainObject.zoneName + '.';
    +    const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
    +
    +    log(`get: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type}`);
    +
    +    const [error, response] = await safe(superagent.get(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
    +        .set('X-API-Key', domainConfig.apiKey)
    +        .timeout(30 * 1000)
    +        .retry(5)
    +        .ok(() => true));
    +
    +    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    +    if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND, formatError(response));
    +    if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
    +    if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
    +
    +    const rrset = response.body.rrsets.find(r => r.name === fqdn && r.type === type);
    +    if (!rrset) return [];
    +
    +    return rrset.records.map(r => {
    +        if (type === 'TXT') return r.content.replace(/^"(.*)"$/, '$1');
    +        return r.content;
    +    });
    +}
    +
    +async function upsert(domainObject, subdomain, type, values) {
    +    assert.strictEqual(typeof domainObject, 'object');
    +    assert.strictEqual(typeof subdomain, 'string');
    +    assert.strictEqual(typeof type, 'string');
    +    assert(Array.isArray(values));
    +
    +    const domainConfig = domainObject.config;
    +    const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
    +    const zoneName = domainObject.zoneName + '.';
    +    const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
    +
    +    log(`upsert: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type} values ${values}`);
    +
    +    const records = values.map(v => {
    +        let content = v;
    +        if (type === 'TXT' && !content.startsWith('"')) content = `"${v}"`;
    +        return { content, disabled: false };
    +    });
    +
    +    const rrset = {
    +        name: fqdn,
    +        type: type,
    +        ttl: 60,
    +        changetype: 'REPLACE',
    +        records: records
    +    };
    +
    +    const [error, response] = await safe(superagent.patch(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
    +        .set('X-API-Key', domainConfig.apiKey)
    +        .send({ rrsets: [rrset] })
    +        .timeout(30 * 1000)
    +        .retry(5)
    +        .ok(() => true));
    +
    +    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    +    if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
    +    if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
    +}
    +
    +async function del(domainObject, subdomain, type, values) {
    +    assert.strictEqual(typeof domainObject, 'object');
    +    assert.strictEqual(typeof subdomain, 'string');
    +    assert.strictEqual(typeof type, 'string');
    +    assert(Array.isArray(values));
    +
    +    const domainConfig = domainObject.config;
    +    const baseUrl = domainConfig.apiUrl.replace(/\/$/, '');
    +    const zoneName = domainObject.zoneName + '.';
    +    const fqdn = dns.fqdn(subdomain, domainObject.domain) + '.';
    +
    +    log(`del: domain ${domainObject.domain} zone ${zoneName} fqdn ${fqdn} type ${type} values ${values}`);
    +
    +    const rrset = {
    +        name: fqdn,
    +        type: type,
    +        changetype: 'DELETE'
    +    };
    +
    +    const [error, response] = await safe(superagent.patch(`${baseUrl}/api/v1/servers/localhost/zones/${zoneName}`)
    +        .set('X-API-Key', domainConfig.apiKey)
    +        .send({ rrsets: [rrset] })
    +        .timeout(30 * 1000)
    +        .retry(5)
    +        .ok(() => true));
    +
    +    if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
    +    if (response.status === 401 || response.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, formatError(response));
    +    if (response.status === 404 || response.status === 422) return;
    +    if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, formatError(response));
    +}
    +
    +async function wait(domainObject, subdomain, type, value, options) {
    +    assert.strictEqual(typeof domainObject, 'object');
    +    assert.strictEqual(typeof subdomain, 'string');
    +    assert.strictEqual(typeof type, 'string');
    +    assert.strictEqual(typeof value, 'string');
    +    assert(options && typeof options === 'object');
    +
    +    const fqdn = dns.fqdn(subdomain, domainObject.domain);
    +    await waitForDns(fqdn, domainObject.zoneName, type, value, options);
    +}
    +
    +async function verifyDomainConfig(domainObject) {
    +    assert.strictEqual(typeof domainObject, 'object');
    +
    +    const domainConfig = domainObject.config;
    +    if (!domainConfig.apiUrl || typeof domainConfig.apiUrl !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'apiUrl must be a non-empty string');
    +    if (!domainConfig.apiKey || typeof domainConfig.apiKey !== 'string') throw new BoxError(BoxError.BAD_FIELD, 'apiKey must be a non-empty string');
    +
    +    const testSubdomain = 'cloudrontestdns';
    +    const testIp = '127.0.0.1';
    +
    +    await upsert(domainObject, testSubdomain, 'A', [testIp]);
    +    await del(domainObject, testSubdomain, 'A', [testIp]);
    +
    +    return {
    +        apiUrl: domainConfig.apiUrl,
    +        apiKey: domainConfig.apiKey
    +    };
    +}
    +
    +export default {
    +    removePrivateFields,
    +    injectPrivateFields,
    +    upsert,
    +    get,
    +    del,
    +    wait,
    +    verifyDomainConfig
    +};
    diff --git a/src/test/dns-providers-test.js b/src/test/dns-providers-test.js
    index 417a2a9ab..454f9803b 100644
    --- a/src/test/dns-providers-test.js
    +++ b/src/test/dns-providers-test.js
    @@ -412,6 +412,79 @@ describe('dns provider', function () {
             const TOKEN = 'sometoken';
             const NAMECOM_API = 'https://api.name.com/v4';
     
    +
    +    describe('powerdns', function () {
    +        const API_URL = 'http://ns1.example.com:8081';
    +        const API_KEY = 'secret';
    +
    +        before(async function () {
    +            domainCopy.provider = 'powerdns';
    +            domainCopy.config = {
    +                apiUrl: API_URL,
    +                apiKey: API_KEY
    +            };
    +
    +            await domains.setConfig(domainCopy.domain, domainCopy, auditSource);
    +        });
    +
    +        it('upsert non-existing record succeeds', async function () {
    +            nock.cleanAll();
    +
    +            const zoneName = domainCopy.zoneName + '.';
    +            const fqdn = 'test.' + domainCopy.domain + '.';
    +
    +            const req1 = nock(API_URL)
    +                .patch('/api/v1/servers/localhost/zones/' + zoneName, body => {
    +                    return body.rrsets[0].name === fqdn &&
    +                           body.rrsets[0].type === 'A' &&
    +                           body.rrsets[0].changetype === 'REPLACE' &&
    +                           body.rrsets[0].records[0].content === '1.2.3.4';
    +                })
    +                .reply(204);
    +
    +            await dns.upsertDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4']);
    +            assert.ok(req1.isDone());
    +        });
    +
    +        it('get succeeds', async function () {
    +            nock.cleanAll();
    +
    +            const zoneName = domainCopy.zoneName + '.';
    +            const fqdn = 'test.' + domainCopy.domain + '.';
    +
    +            const req1 = nock(API_URL)
    +                .get('/api/v1/servers/localhost/zones/' + zoneName)
    +                .reply(200, {
    +                    rrsets: [{
    +                        name: fqdn,
    +                        type: 'A',
    +                        records: [{ content: '1.2.3.4', disabled: false }]
    +                    }]
    +                });
    +
    +            const result = await dns.getDnsRecords('test', domainCopy.domain, 'A');
    +            assert.deepEqual(result, ['1.2.3.4']);
    +            assert.ok(req1.isDone());
    +        });
    +
    +        it('del succeeds', async function () {
    +            nock.cleanAll();
    +
    +            const zoneName = domainCopy.zoneName + '.';
    +            const fqdn = 'test.' + domainCopy.domain + '.';
    +
    +            const req1 = nock(API_URL)
    +                .patch('/api/v1/servers/localhost/zones/' + zoneName, body => {
    +                    return body.rrsets[0].name === fqdn &&
    +                           body.rrsets[0].type === 'A' &&
    +                           body.rrsets[0].changetype === 'DELETE';
    +                })
    +                .reply(204);
    +
    +            await dns.removeDnsRecords('test', domainCopy.domain, 'A', ['1.2.3.4']);
    +            assert.ok(req1.isDone());
    +        });
    +    });
             before(async function () {
                 domainCopy.provider = 'namecom';
                 domainCopy.config = {
    
    
    Feature Requests

  • Support PowerDNS Provider
    J jk

    Hi @james
    I have access to cloudron, but it looks like I can neither fork, nor create my own repo at the moment. Could you check?

    Feature Requests

  • Support PowerDNS Provider
    J jk

    Certainly.

    Feature Requests

  • Support PowerDNS Provider
    J jk

    @james said:

    can you provide your changes in a merge request so we can have a look?

    I'd be happy to. How do I get access to git.cloudron.io again?

    Feature Requests

  • Support PowerDNS Provider
    J jk

    @james said:

    You can always edit the code directly in /home/yellowtent/box/ and after the edit you need to restart the box.service with systemctl restart box.service

    Yes, I've done that in the past to take care of some tiny issues with the proxy.

    Would this also automatically work with changes to the Vue code? Because that's usually compiled right? Part of the patch is adding support for PowerDNS in the UI.

    Feature Requests

  • Open Source DNS/Nameserver App?
    J jk

    I'm running my own setup using PowerDNS Authoritative API for many years now, and I use the very simple PowerDNS WebUI. The entirety of it is a single HTML page.

    I have it protected with Basic auth, after which the PowerDNS API route gets the X-API key injected automatically. That helps me to separate user auth from server auth.

    When packaged for Cloudron, I would imagine the frontend would be put behind the auth proxy. That might be the simplest possible set-up. However, it would effectively be single-user in the sense that every user can see and modify all domains on the PowerDNS instance.

    PowerDNS-Admin and so on would indeed be a good option if true multi-user is required, but also requires quite a bit more packaging and maintenance efforts. This package however seems to be in maintenance mode and is in the process of being replaced by a seemingly way more complex system called dnsmin.
    .

    App Wishlist

  • Support PowerDNS Provider
    J jk

    Since I've run a PowerDNS Authoritative DNS for quite a while now, I've been interested in this feature as well. Cloudron not supporting this is the only thing that has prevented me to move my cloudron-managed domains away from cloud providers.

    I've made several private integrations with the API in the past, but Javascript has held me back from trying my hand at this for Cloudron. Now, with the help of an LLM, I probably have a working implementation. At least it looks good from the API calls perspective.

    So I have two questions for staff:

    1. How can I test it (preferably on a fresh VM, so as to not put my production Cloudron in jeopardy)? I could not really find build/test/deploy instructions in the box repo.

    2. Would you be interested in this implementation if it is tested and works? If you are, I fully intend to give you this code under the terms of your license as show in the box repo. It's in a private repo for now since I seem to have lost the account I once had on git.cloudron.io.

    Thank you in advance!

    Feature Requests

  • Problem migrating data
    J jk

    That worked, this is solved now. Thank you.

    Support restore migration

  • Problem migrating data
    J jk

    @joseph It took a while, and one of the two is now restored.

    I have an issue still with Immich though: it seems that the container version that was specified in the backup cannot be found:

    No such image: cloudron/app.immich.cloudronapp:202603270614470000

    Apr 08 00:11:43 apptask: createContainer: creating container
    Apr 08 00:11:43 apptask: run: app error for state pending_import: BoxError: (HTTP code 404) no such container - No such image: cloudron/app.immich.cloudronapp:202603270614470000
    Apr 08 00:11:43 tasks: setCompleted - 20478: {"result":null,"error":{"message":"(HTTP code 404) no such container - No such image: cloudron/app.immich.cloudronapp:202603270614470000 ","reason":"Docker Error"},"percent":100}
    Apr 08 00:11:43 tasks: updating task 20478 with: {"completed":true,"result":null,"error":{"message":"(HTTP code 404) no such container - No such image: cloudron/app.immich.cloudronapp:202603270614470000 ","reason":"Docker Error"},"percent":100}
    

    So now I'm stuck again.

    Support restore migration

  • Problem migrating data
    J jk

    Hi,

    I just moved to a new machine. As usual, the restoration process went fairly smoothly.

    There is one issue remaining though, impacting two apps.

    Both were configured to use an external media mount (the kind that is included in backup), which was a secondary disk. That disk is unavailable on the new machine.

    For these two apps, the restoration failed, because it this disk could not be found. I tried to set the apps storage to use the default storage, but I couldn't because they are in error state.

    What do I need to do to fix this issue and get the apps up-and-running again with their data?

    Output of cloudron-support --troubleshoot

    Vendor: QEMU Product: Standard PC (Q35 + ICH9, 2009)
    Linux: 6.8.0-107-generic
    Ubuntu: noble 24.04
    Execution environment: kvm
    Processor: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
    BIOS pc-q35-10.2  CPU @ 2.0GHz x 4
    RAM: 12177808KB
    Disk: /dev/sda2        45G
    [OK]    node version is correct
    [OK]    IPv6 is enabled and public IPv6 address is working
    [OK]    docker is running
    [OK]    docker version is correct
    [OK]    MySQL is running
    [OK]    netplan is good
    [OK]    DNS is resolving via systemd-resolved
    [OK]    unbound is running
    [OK]    nginx is running
    [OK]    dashboard cert is valid
    [OK]    dashboard is reachable via loopback
    [OK]    No pending database migrations
    [OK]    Service 'mysql' is running and healthy
    [OK]    Service 'postgresql' is running and healthy
    [OK]    Service 'mongodb' is running and healthy
    [OK]    Service 'mail' is running and healthy
    [OK]    Service 'graphite' is running and healthy
    [OK]    Service 'sftp' is running and healthy
    [OK]    box v9.1.5 is running
    [OK]    Dashboard is reachable via domain name
    [WARN]  Domain <redacted> expiry check skipped because whois does not have this information
    

    Excerpt from the app log:

    2026-04-02T22:00:31.732Z tasks: updating task 20419 with: {"percent":65,"message":"Downloading 15488M@6MBps"}
    2026-04-02T22:00:38.028Z tasks: updating task 20419 with: {"percent":65,"message":"Still downloading backup (3721s, 15488M)"}
    2026-04-02T22:00:41.737Z tasks: updating task 20419 with: {"percent":65,"message":"Downloading 15552M@6MBps"}
    2026-04-02T22:00:50.832Z backupformat/tgz: tarExtract: extracted 217597 entries
    2026-04-02T22:00:50.833Z backupformat/tgz: tarExtract: pipeline finished: {"startTime":1775163517410,"duration":3733423,"transferred":16364142900}
    2026-04-02T22:00:50.833Z backuptask: downloadApp: time: 3751.965
    2026-04-02T22:00:50.833Z tasks: updating task 20419 with: {"percent":70,"message":"Restoring addons"}
    2026-04-02T22:00:51.395Z services: restoreAddons: restoring ["postgresql","ldap","sendmail","oidc","redis","localstorage","scheduler","turn"]
    2026-04-02T22:00:51.396Z services: Restore postgresql
    2026-04-02T22:02:41.597Z services: Setting up LDAP
    2026-04-02T22:02:41.726Z services: Setting up SendMail
    2026-04-02T22:02:41.727Z services: Setting sendmail addon config to [{"name":"CLOUDRON_MAIL_SMTP_SERVER","value":"mail"},{"name":"CLOUDRON_MAIL_SMTP_PORT","value":"2525"},{"name":"CLOUDRON_MAIL_SMTPS_PORT","value":"2465"},{"name":"CLOUDRON_MAIL_STARTTLS_PORT","value":"2587"},{"name":"CLOUDRON_MAIL_SMTP_USERNAME","value":"nextcloud.app@<redacted>"},{"name":"CLOUDRON_MAIL_SMTP_PASSWORD","value":"<redacted>"},{"name":"CLOUDRON_MAIL_FROM","value":"nextcloud.app@<redacted>"},{"name":"CLOUDRON_MAIL_DOMAIN","value":"<redacted>"}]
    2026-04-02T22:02:41.761Z services: Setting up OIDC
    2026-04-02T22:02:41.787Z services: Restoring redis
    2026-04-02T22:02:41Z [POST] /restore
    2026-04-02T22:02:41Z restoring
    2026-04-02T22:02:42Z 13:M 02 Apr 2026 22:02:42.062 * BGSAVE done, 0 keys saved, 0 keys skipped, 88 bytes written.
    2026-04-02T22:02:42Z 13:M 02 Apr 2026 22:02:42.062 * Saving the final RDB snapshot before exiting.
    2026-04-02T22:02:42Z 13:M 02 Apr 2026 22:02:42.062 * User requested shutdown...
    2026-04-02T22:02:42Z 13:M 02 Apr 2026 22:02:42.208 # Redis is now ready to exit, bye bye...
    2026-04-02T22:02:42Z 13:M 02 Apr 2026 22:02:42.208 * DB saved on disk
    2026-04-02T22:02:42Z 13:M 02 Apr 2026 22:02:42.208 * Removing the pid file.
    2026-04-02T22:02:42Z 13:signal-handler (1775167362) Received SIGTERM scheduling shutdown...
    2026-04-02T22:02:42Z 2026-04-02 22:02:42,019 INFO waiting for redis to stop
    2026-04-02T22:02:42Z 2026-04-02 22:02:42,210 INFO stopped: redis (exit status 0)
    2026-04-02T22:02:42Z 2026-04-02 22:02:42,997 INFO spawned: 'redis' with pid 46
    2026-04-02T22:02:43Z 46:C 02 Apr 2026 22:02:43.017 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
    2026-04-02T22:02:43Z 46:C 02 Apr 2026 22:02:43.017 * Configuration loaded
    2026-04-02T22:02:43Z 46:C 02 Apr 2026 22:02:43.017 * Redis version=8.4.0, bits=64, commit=00000000, modified=1, pid=46, just started
    2026-04-02T22:02:43Z 46:C 02 Apr 2026 22:02:43.017 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.018 # Failed to write PID file: Permission denied
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.018 * Increased maximum number of open files to 10032 (it was originally set to 1024).
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.018 * Running mode=standalone, port=6379.
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.018 * monotonic clock: POSIX clock_gettime
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.019 * Loading RDB produced by version 8.4.0
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.019 * RDB age 14982 seconds
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.019 * RDB memory usage when created 37.47 Mb
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.019 * Server initialized
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.171 * DB loaded from disk: 0.153 seconds
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.171 * Done loading RDB, keys loaded: 29201, keys expired: 53.
    2026-04-02T22:02:43Z 46:M 02 Apr 2026 22:02:43.171 * Ready to accept connections tcp
    2026-04-02T22:02:44.212Z tasks: updating task 20419 with: {"percent":80,"message":"Creating container"}
    2026-04-02T22:02:44.241Z apptask: createContainer: creating container
    2026-04-02T22:02:44.246Z shell: mounts: mountpoint -q -- /mnt/volumes/d8d52065be8d499f85c1e566498693bb
    2026-04-02T22:02:44.260Z shell: mounts: mountpoint -q -- /mnt/volumes/d8d52065be8d499f85c1e566498693bb errored BoxError: mountpoint exited with code 32 signal null
    2026-04-02T22:02:44.261Z shell: mounts: systemd-escape -p --suffix=mount /mnt/volumes/d8d52065be8d499f85c1e566498693bb
    2026-04-02T22:02:44.273Z shell: mounts: journalctl -u mnt-volumes-d8d52065be8d499f85c1e566498693bb.mount\n -n 10 --no-pager -o json
    2026-04-02T22:02:44.286Z apptask: run: app error for state pending_restore: BoxError: Storage volume "External Data" is not active. Could not determine mount failure reason. 
    2026-04-02T22:02:44.325Z tasks: setCompleted - 20419: {"result":null,"error":{"message":"Storage volume \"External Data\" is not active. Could not determine mount failure reason. ","reason":"Bad State"},"percent":100}
    2026-04-02T22:02:44.325Z tasks: updating task 20419 with: {"completed":true,"result":null,"error":{"message":"Storage volume \"External Data\" is not active. Could not determine mount failure reason. ","reason":"Bad State"},"percent":100}
    2026-04-02T22:02:44.342Z Exiting with code 0
    2026-04-02T22:02:44.342Z taskworker: Task took 4088.725 seconds
    2026-04-02T22:02:44Z 2026-04-02 22:02:44,173 INFO success: redis entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
    2026-04-04T10:05:01Z [GET] /healthcheck
    BoxError: Storage volume "External Data" is not active. Could not determine mount failure reason. 
    pipeFileToRequest: piped /home/yellowtent/appsdata/b62559e1-63a2-443c-8afc-cf91c3f4faab/dump.rdb, waiting for response
    pipeFileToRequest: piped /home/yellowtent/appsdata/b62559e1-63a2-443c-8afc-cf91c3f4faab/postgresqldump, waiting for response
    pipeFileToRequest: piping /home/yellowtent/appsdata/b62559e1-63a2-443c-8afc-cf91c3f4faab/dump.rdb
    pipeFileToRequest: piping /home/yellowtent/appsdata/b62559e1-63a2-443c-8afc-cf91c3f4faab/postgresqldump
    pipeFileToRequest: response status code 200
    pipeFileToRequest: response status code 200
    pipeFileToRequest: success
    pipeFileToRequest: success
    }
    }
    
    Support restore migration

  • Cannot install apps from docker-registry because authentication fails
    J jk

    Thanks a lot!

    Docker Registry docker registry authentication

  • Cannot install apps from docker-registry because authentication fails
    J jk

    Hi, I have encountered this bug once more, with the following user agents:

    • Podman: libpod/<version>
    • Skopeo: skopeo/<version>

    @girish Would it be possible to add those as well? That would be much appreciated.

    In src/proxyAuth.js

    // someday this can be more sophisticated and check for a real browser
    function isBrowser(req) {
        const userAgent = req.get('user-agent');
        if (!userAgent) return false;
    
        // https://github.com/docker/engine/blob/master/dockerversion/useragent.go#L18
        return !userAgent.toLowerCase().includes('docker') && !userAgent.toLowerCase().includes('container') && !userAgent.toLowerCase().includes('libpod') && !userAgent.toLowerCase().includes('skopeo');
    }
    

    In src/nginxconfig.ejs

        location @proxy-auth-login {
            if ($http_user_agent ~* "docker") {
                return 401;
            }
            if ($http_user_agent ~* "container") {
                return 401;
            }
            if ($http_user_agent ~* "libpod") {
                return 401;
            }
            if ($http_user_agent ~* "skopeo") {
                return 401;
            }
    
            return 302 /login?redirect=$request_uri;
        }
    
    Docker Registry docker registry authentication

  • Firefly III - Package Updates
    J jk

    @girish Minor thing, but the links to the "Full Changelog" entries for the last releases seem to go to random Github user profiles.

    Firefly III

  • IPv6 only Cloudron
    J jk

    Also, if this gets more development, I'd be quite happy to help test it.

    Feature Requests

  • IPv6 only Cloudron
    J jk

    I have tried this a few months ago as well.

    The next blocker is that the setup code does some domain IP validation, and refuses to continue of there are no IPv4 addresses available. I worked around it in the setup code of the box project on my machine, but that is of course highly discouraged 😏. Sadly, I lost that code (it wasn't too bad). I'm not entirely sure whether it works with a private IPv4. I haven't tested that. I don't think so though.

    Once the box supports primary IPv6, then the rest will more or less work. Because it support IPv6 mostly fine after setup. Thinks like the web front-end, and mail all work with IPv6.

    Individual apps may or may not work if they do something other than standard HTTP(S). The OpenVPN app for example does not work correctly yet. At least it does not route IPv6 through the tunnel, possibly because the Docker containers are all IPv4-only.

    Feature Requests

  • Package 1.20.0-1 broke
    J jk

    Sure, I just did.

    FreshRSS

  • Package 1.20.0-1 broke
    J jk

    Hi,

    Last night's automatic update of FreshRSS (package version 1.20.0-1) has broken my setup, due to a TypeError from PHP. Version 1.20.0 worked fine.

    Here is an excerpt from the logs:

    2023-12-27T03:02:58.000Z FreshRSS starting feeds actualization at 2023-12-27T03:02:58+00:00
    2023-12-27T03:02:58.000Z PHP Fatal error:  Uncaught TypeError: Minz_ExtensionManager::enable(): Argument #1 ($ext_name) must be of type string, int given, called in /app/code/lib/Minz/ExtensionManager.php on line 279 and defined in /app/code/lib/Minz/ExtensionManager.php:248
    2023-12-27T03:02:58.000Z Results:
    2023-12-27T03:02:58.000Z Stack trace:
    2023-12-27T03:02:58.000Z thrown in /app/code/lib/Minz/ExtensionManager.php on line 248
    2023-12-27T03:03:00.000Z - - - [27/Dec/2023:03:03:00 +0000] "GET / HTTP/1.1" 302 - "-" "Mozilla (CloudronHealth)"
    2023-12-27T03:03:10.000Z - - - [27/Dec/2023:03:03:10 +0000] "GET / HTTP/1.1" 302 - "-" "Mozilla (CloudronHealth)"
    2023-12-27T03:03:20.000Z - - - [27/Dec/2023:03:03:20 +0000] "GET / HTTP/1.1" 302 - "-" "Mozilla (CloudronHealth)"
    2023-12-27T03:03:30.000Z - - - [27/Dec/2023:03:03:30 +0000] "GET / HTTP/1.1" 302 - "-" "Mozilla (CloudronHealth)"
    2023-12-27T03:03:40.000Z - - - [27/Dec/2023:03:03:40 +0000] "GET / HTTP/1.1" 302 - "-" "Mozilla (CloudronHealth)"
    2023-12-27T03:03:50.000Z - - - [27/Dec/2023:03:03:50 +0000] "GET / HTTP/1.1" 302 - "-" "Mozilla (CloudronHealth)"
    2023-12-27T03:03:58.000Z #0 /app/code/lib/Minz/ExtensionManager.php(279): Minz_ExtensionManager::enable()
    2023-12-27T03:03:58.000Z #1 /app/code/app/FreshRSS.php(63): Minz_ExtensionManager::enableByList()
    2023-12-27T03:03:58.000Z #2 /app/code/app/actualize_script.php(97): FreshRSS->init()
    2023-12-27T03:03:58.000Z #3 {main}
    2023-12-27T03:03:58.000Z ==> Run actualize script
    2023-12-27T03:03:58.000Z FreshRSS starting feeds actualization at 2023-12-27T03:03:58+00:00
    2023-12-27T03:03:58.000Z PHP Fatal error:  Uncaught TypeError: Minz_ExtensionManager::enable(): Argument #1 ($ext_name) must be of type string, int given, called in /app/code/lib/Minz/ExtensionManager.php on line 279 and defined in /app/code/lib/Minz/ExtensionManager.php:248
    2023-12-27T03:03:58.000Z Results:
    2023-12-27T03:03:58.000Z Stack trace:
    2023-12-27T03:03:58.000Z thrown in /app/code/lib/Minz/ExtensionManager.php on line 248
    

    Thankfully, restoring a backup made everything work again.

    FreshRSS
  • Login

  • Don't have an account? Register

  • Login or register to search.
  • First post
    Last post
0
  • Categories
  • Recent
  • Tags
  • Popular
  • Bookmarks
  • Search