<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: John Bannister</title>
    <description>The latest articles on DEV Community by John Bannister (@eudorajab1).</description>
    <link>https://dev.to/eudorajab1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4189171%2F59bae7e4-f3c2-43ee-9cdc-b5f2d80a67ce.jpg</url>
      <title>DEV Community: John Bannister</title>
      <link>https://dev.to/eudorajab1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eudorajab1"/>
    <language>en</language>
    <item>
      <title>Edit a WordPress theme over SSH from VS Code, Cursor, JetBrains or Zed</title>
      <dc:creator>John Bannister</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:16:54 +0000</pubDate>
      <link>https://dev.to/eudorajab1/edit-a-wordpress-theme-over-ssh-from-vs-code-cursor-jetbrains-or-zed-1c39</link>
      <guid>https://dev.to/eudorajab1/edit-a-wordpress-theme-over-ssh-from-vs-code-cursor-jetbrains-or-zed-1c39</guid>
      <description>&lt;p&gt;Work on a WordPress site's theme in the editor you already use, on the real site's files, with the change on the site the moment you save. Here is how to switch SSH on, connect each of the four common editors, and make a first change to a theme, on Uppcoder's free plan too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;p&gt;Three things, all free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A WordPress site on Uppcoder. The free plan has one: Stacks, Create, From a template, WordPress + MariaDB.&lt;/li&gt;
&lt;li&gt;An SSH client. macOS, Linux and Windows 10 and 11 have one built in (ssh in Terminal or PowerShell).&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One of the editors below, or just the terminal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://portal.uppcoder.com/guides/free-wordpress-test-site?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=posts&amp;amp;utm_content=post-219" rel="noopener noreferrer"&gt;No WordPress site yet? A free WordPress test site that does not expire&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Switch SSH on
&lt;/h2&gt;

&lt;p&gt;Open the stack, then Add to stack, and click the WordPress card. Tick SSH (and VS Code (with terminal) too if you also want an editor in the browser), then Apply. SSH is added beside WordPress: the site does not restart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39qkfz8genn34utf70dy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39qkfz8genn34utf70dy.jpg" alt="The WordPress card: tick SSH, and VS Code (with terminal) if you want it too" width="798" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A key, and one block for ~/.ssh/config
&lt;/h2&gt;

&lt;p&gt;Back on the stack's page, open Connect with SSH. Create a key for me makes a key: the public half goes to your account and the private half downloads to your computer. Or paste a key you already have in Settings, SSH Keys. Then copy the block under "For your editor" into ~/.ssh/config (on Windows, .ssh\config in your user folder). With a key the page made, the block ends with an IdentityFile line pointing at the download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; wordpress
    &lt;span class="k"&gt;HostName&lt;/span&gt; &amp;lt;your-site&amp;gt;-editor.&amp;lt;server&amp;gt;.websson.cloud
    &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2222&lt;/span&gt;
    &lt;span class="k"&gt;User&lt;/span&gt; &amp;lt;your-site&amp;gt;
    &lt;span class="k"&gt;StrictHostKeyChecking&lt;/span&gt; accept-new
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/Downloads/uppcoder_&amp;lt;date&amp;gt;_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhpxokmp0gmfyeaqtoag5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhpxokmp0gmfyeaqtoag5.jpg" alt="Connect with SSH on the stack page: the command for a terminal, and the block for ~/.ssh/config" width="798" height="190"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Check it from a terminal
&lt;/h2&gt;

&lt;p&gt;One command, then you are in the site's folder, as the web server's own user, so every file you save keeps the owner WordPress expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh wordpress
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;
/var/www/html
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;wp-content/themes
index.php  twentytwentyfive  twentytwentyfour  twentytwentythree
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connect your editor
&lt;/h2&gt;

&lt;p&gt;Each editor uses the same "wordpress" entry. Open the folder /var/www/html once you are connected. From each editor's own docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VS Code: install the Remote - SSH extension (ms-vscode-remote.remote-ssh), run Remote-SSH: Connect to Host... from the Command Palette and pick wordpress, then File, Open... and /var/www/html.&lt;/li&gt;
&lt;li&gt;Cursor: Cursor has its own Remote - SSH extension, published by Anysphere, the company behind Cursor. Install it from the Extensions view, then Remote-SSH: Connect to Host... as in VS Code.&lt;/li&gt;
&lt;li&gt;JetBrains Gateway (or Remote Development in a JetBrains IDE): SSH, New Connection, give the host, port 2222 and user from the block above or let it read ~/.ssh/config, Check Connection and Continue, choose the IDE, set the project to /var/www/html, then Upload IDE and Connect.&lt;/li&gt;
&lt;li&gt;Zed: open Remote Projects (ctrl-cmd-shift-o on macOS, alt-ctrl-shift-o elsewhere), Connect New Server, type ssh wordpress, and choose /var/www/html. From a terminal: zed ssh://wordpress/var/www/html&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each editor puts a server part of its own in your home folder on the site the first time it connects. That folder is outside the site, so none of it is served or mixed into your theme.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://code.visualstudio.com/docs/remote/ssh" rel="noopener noreferrer"&gt;VS Code: Remote Development using SSH&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.jetbrains.com/help/idea/remote-development-a.html" rel="noopener noreferrer"&gt;JetBrains: connect to a remote server from Gateway&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://zed.dev/docs/remote-development" rel="noopener noreferrer"&gt;Zed: Remote Development&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A first change to the theme
&lt;/h2&gt;

&lt;p&gt;Open wp-content/themes//functions.php and add a few lines at the end. This one prints a line in the footer of every page; save, reload the site, and it is there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_footer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;p style="text-align:center;padding:1em;'&lt;/span&gt;
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'background:#fde68a;margin:0"&amp;gt;'&lt;/span&gt;
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'Edited over SSH from my own editor&amp;lt;/p&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0kdycxxwiqchj4a3cjo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0kdycxxwiqchj4a3cjo.jpg" alt="The site after saving functions.php: the new line at the bottom of the page" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Before real work: a child theme
&lt;/h2&gt;

&lt;p&gt;Edit a child theme, not the theme itself. WordPress's own handbook says why: change the parent theme directly and the next time it is updated, your change will disappear. A child theme keeps your functions.php and styles through updates, and its functions.php is loaded as well as the parent's.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.wordpress.org/themes/advanced-topics/child-themes/" rel="noopener noreferrer"&gt;WordPress Theme Handbook: child themes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Good to know
&lt;/h2&gt;

&lt;p&gt;Said up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH comes with every plan, the free plan too, for the stack's owner&lt;/li&gt;
&lt;li&gt;A mistake in functions.php can stop the site. SSH does not go through WordPress, so you can still log in and undo it&lt;/li&gt;
&lt;li&gt;git is there for your theme folder; PHP and WP-CLI are not part of the SSH session&lt;/li&gt;
&lt;li&gt;After the editor beside your site restarts, each editor sets up its server again on the next connect, which takes a moment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A child theme to start from: &lt;a href="https://github.com/Uppcoder/wordpress-child-theme-starter" rel="noopener noreferrer"&gt;Uppcoder/wordpress-child-theme-starter&lt;/a&gt; on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First published at &lt;a href="https://portal.uppcoder.com/guides/edit-wordpress-theme-over-ssh" rel="noopener noreferrer"&gt;https://portal.uppcoder.com/guides/edit-wordpress-theme-over-ssh&lt;/a&gt;&lt;/em&gt;?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=posts&amp;amp;utm_content=post-219&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>ssh</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A free WordPress test site that does not expire</title>
      <dc:creator>John Bannister</dc:creator>
      <pubDate>Thu, 05 Nov 2026 10:14:03 +0000</pubDate>
      <link>https://dev.to/eudorajab1/a-free-wordpress-test-site-that-does-not-expire-9hl</link>
      <guid>https://dev.to/eudorajab1/a-free-wordpress-test-site-that-does-not-expire-9hl</guid>
      <description>&lt;p&gt;Most free WordPress test sites are gone in a day or a week, or live only on your own computer. Here is how the common options compare, when one of them is the better pick, and how to get an online WordPress with its own database, VS Code and SSH that is still there next month, free and with no card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four questions to ask
&lt;/h2&gt;

&lt;p&gt;Before you put work into a free WordPress test site, check these on the provider's own page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it expire? Many free test sites are deleted after a day or a week.&lt;/li&gt;
&lt;li&gt;Is it online? A site on your own computer is fine for you, but a client or a colleague cannot open it.&lt;/li&gt;
&lt;li&gt;Can you reach the code and the database? Theme and plugin work needs the files, a terminal or SSH, and the database.&lt;/li&gt;
&lt;li&gt;Does it need a card? Some services ask for one before the first site.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Online test sites: how long the free ones last
&lt;/h2&gt;

&lt;p&gt;From each provider's own page, checked on 4 November 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ZipWP: free sandbox sites stay active for 24 hours (zipwp.com/pricing).&lt;/li&gt;
&lt;li&gt;TasteWP: a free site lasts 2 days, or 7 days if you are logged in; no expiry from USD 2.98 a month (tastewp.com).&lt;/li&gt;
&lt;li&gt;SandyWP: free sandboxes expire after 7 days and are deleted with their files and database; SSH comes with the USD 9 a month plan (sandywp.com).&lt;/li&gt;
&lt;li&gt;WPSandbox: free temporary sites without an account; longer or no expiry on paid plans (wpsandbox.net).&lt;/li&gt;
&lt;li&gt;InstaWP: no free plan; a card is needed to start a site, and the cheapest site is USD 2 a month (instawp.com/pricing).&lt;/li&gt;
&lt;li&gt;Studio by WordPress.com: free preview sites stay up for 7 days after the last update, up to ten at a time (developer.wordpress.com/studio).&lt;/li&gt;
&lt;li&gt;Pantheon: two free sandbox sites; a sandbox older than four months with no Git activity for three months is frozen, and going live needs a paid plan (pantheon.io/plans/pricing).&lt;/li&gt;
&lt;li&gt;Uppcoder Spark: no expiry and no card; its own MariaDB database, VS Code in the browser and SSH; backed up every night.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Free and permanent, but not online
&lt;/h2&gt;

&lt;p&gt;These never expire, because they run on your own machine. Nobody else can open the site unless you share it from there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress Playground runs WordPress inside your browser. A temporary site is lost when you close or refresh the tab; a saved one is kept in browser storage, which the browser may clear (developer.wordpress.org/playground).&lt;/li&gt;
&lt;li&gt;LocalWP is a free desktop app with SSH and WP-CLI; Live Links share a site by giving others access to your computer (localwp.com).&lt;/li&gt;
&lt;li&gt;DevKinsta is free and runs on your computer in Docker, with Adminer for the database (kinsta.com/devkinsta).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When something else is the better pick
&lt;/h2&gt;

&lt;p&gt;Said plainly, because the right tool depends on the job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trying a plugin for five minutes: WordPress Playground. Nothing to sign up for, and nothing to clean up.&lt;/li&gt;
&lt;li&gt;Working offline on your own machine: LocalWP, Studio or DevKinsta.&lt;/li&gt;
&lt;li&gt;A plain WordPress site for each student on a course: WordPress.com's Student Plan, free for the first year when a teacher applies (wordpress.com/blog, 17 August 2026).&lt;/li&gt;
&lt;li&gt;Copying a live site into a test copy with one click: InstaWP, Studio and DevKinsta have that. Uppcoder has no such button; you move the site yourself, for example with a backup plugin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you get on Uppcoder's free plan
&lt;/h2&gt;

&lt;p&gt;The free plan (Spark) runs one app with one database. No card needed, and no time limit: if we ever change or end it, you hear from us by email at least 60 days before. For WordPress that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress, the official release with WP-CLI, installed with the admin login you choose&lt;/li&gt;
&lt;li&gt;Its own MariaDB 11 database, connected for you&lt;/li&gt;
&lt;li&gt;Its own HTTPS address from the first minute, private until you switch it to public&lt;/li&gt;
&lt;li&gt;VS Code in the browser on the site's files, and SSH for your own editor (VS Code Remote, Cursor, JetBrains Gateway, Zed), on the free plan too&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Core, themes, plugins and uploads kept on disk, and backed up every night to a separate server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://websson-doo.com/en/blog/multilingual-hospitality-websites/" rel="noopener noreferrer"&gt;Websson, the company behind Uppcoder, runs its own WordPress sites on the same platform: lessons from Villa Bavigo&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://portal.uppcoder.com/guides/edit-wordpress-theme-over-ssh" rel="noopener noreferrer"&gt;Next: edit the theme over SSH from VS Code, Cursor, JetBrains or Zed&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start in a few minutes
&lt;/h2&gt;

&lt;p&gt;Sign up at portal.uppcoder.com and start the free plan (confirm your email first). Then Stacks, Create, From a template, WordPress + MariaDB. Give it a name, choose the admin password, and Deploy. For code, open the WordPress card and tick VS Code (with terminal), SSH, or both; WordPress keeps running while they come on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswacjz07qi3n1vmixi6y.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswacjz07qi3n1vmixi6y.jpg" alt="Picking the WordPress + MariaDB template" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39qkfz8genn34utf70dy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39qkfz8genn34utf70dy.jpg" alt="The WordPress card: tick VS Code (with terminal) and SSH" width="798" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshevj99zw27v1d8daywz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshevj99zw27v1d8daywz.jpg" alt="VS Code in the browser on the site's files, the theme's functions.php open" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Good to know
&lt;/h2&gt;

&lt;p&gt;Said up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The free plan runs one app with its database; WordPress and its MariaDB count as that one app&lt;/li&gt;
&lt;li&gt;Your own domain needs a paid plan; the free site has its own uppcoder address&lt;/li&gt;
&lt;li&gt;WordPress sends no email until you add an SMTP plugin with your mail provider&lt;/li&gt;
&lt;li&gt;One WordPress site; a multisite network is not set up for you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;First published at &lt;a href="https://portal.uppcoder.com/guides/free-wordpress-test-site" rel="noopener noreferrer"&gt;https://portal.uppcoder.com/guides/free-wordpress-test-site&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>php</category>
      <category>hosting</category>
    </item>
    <item>
      <title>Apps run until you stop them</title>
      <dc:creator>John Bannister</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:41:38 +0000</pubDate>
      <link>https://dev.to/eudorajab1/apps-run-until-you-stop-them-3k4c</link>
      <guid>https://dev.to/eudorajab1/apps-run-until-you-stop-them-3k4c</guid>
      <description>&lt;p&gt;&lt;em&gt;I work at Websson, the company that develops StreamGen, so read this with that in mind.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few weeks ago someone asked on r/snowflake whether anyone had real enterprise experience with Snowflake's App Runtime. The product manager for App Runtime answered in the thread, and two of his lines stuck with me. "Apps run until you stop them." And: "You see usage per account, not per app."&lt;/p&gt;

&lt;p&gt;Neither is a complaint. It is how the platform works, and he said it straight. But put the two next to each other and you get the problem StreamGen was built around. Somebody builds a Streamlit app in Snowsight for a quarterly review. It works, people use it for a month, and then nobody opens it again. It still exists, and nobody is sure it can go. And when finance asks what the apps cost, the honest answer is a number for the whole account.&lt;/p&gt;

&lt;p&gt;Multiply that by a team where analysts are now building apps by describing them to an AI, and the list grows faster than anyone can keep track of.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the account already tells you
&lt;/h2&gt;

&lt;p&gt;Some of the raw material is in the account. Snowflake keeps query history and the credits each query used. But none of it is per app, and turning it into a per-app answer means matching queries to apps, deciding how a shared warehouse splits, and doing it all again next month. For JavaScript apps on App Runtime it can't be done at all: they share one compute pool, and Snowflake says per-app cost attribution isn't available.&lt;/p&gt;

&lt;p&gt;And even with a number, someone has to look at it every month and decide that the app nobody opened since July can go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;StreamGen started as a tool that writes Streamlit apps on your own data from a sentence. The more apps it built, the more the question turned into the one above: fine, but which of these are still used, and what do they cost?&lt;/p&gt;

&lt;p&gt;So Admin → Inventory is now the heart of it. Every Streamlit and App Runtime app the role can see, built in StreamGen or not: owner, last deploy, last use, who viewed it in the last 30 days, and the warehouse credits it used. Apps nobody has opened for 60 days get flagged, with what it would take to retire them. StreamGen never drops one itself; that stays your call.&lt;/p&gt;

&lt;p&gt;Then the cost side. Each app shows what it costs, in money once you enter your price per credit, and StreamGen's own compute and the pool its apps share get their own lines, so nothing hides in the total. Apps can be switched off at a set time, say 20:00 on workdays, or after a few hours without viewers, and they start again when someone opens them. That is our answer to "apps run until you stop them": something stops them. There is also a monthly budget that emails the admins at 80% and 100%. It only alerts. I didn't want a budget that switches off an app in the middle of someone's board meeting.&lt;/p&gt;

&lt;p&gt;Every night it also runs each app's queries against your data, so a dropped column or a lost grant shows up in the inventory before a user finds it, and checks the packages the apps use against known vulnerabilities.&lt;/p&gt;

&lt;p&gt;It runs as a Snowflake Native App, inside the customer's account. It acts as the signed-in user and never with more rights than that user has. The data doesn't leave the account. It connects out only to hosts the admin approves: our licence server, and optionally GitHub and a vulnerability database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting through the Marketplace review
&lt;/h2&gt;

&lt;p&gt;StreamGen went live on the Snowflake Marketplace on 23 October. The review was fair and mostly quiet. The one thing they sent back was our SQL examples. We had written four "Quick Start" queries, and three of them were GRANT statements and install steps. Their point was that examples should show using the app, not setting it up. They were right. The grants were already in the app's README, so we replaced the examples with four calls a consumer actually runs: open StreamGen, check that it is running, read its recent log lines, and switch it and its apps off. Resubmitted, approved.&lt;/p&gt;

&lt;p&gt;If you are preparing a listing: no database name in the example identifiers (the consumer names the app at install), one statement per example, and each one has to return at least one row, because they are validated automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try it
&lt;/h2&gt;

&lt;p&gt;Here is &lt;a href="https://app.snowflake.com/marketplace/listing/GZSYZ1QF5MJ/streamgen-streamgen" rel="noopener noreferrer"&gt;the listing on the Snowflake Marketplace&lt;/a&gt;. You need a paid Snowflake account in a region that runs apps with containers. Installing is free and it is activated with a licence key; there is a &lt;a href="https://streamgen.io/trial/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=posts&amp;amp;utm_content=post-139" rel="noopener noreferrer"&gt;free 7-day trial key&lt;/a&gt;, counted from the day you activate it.&lt;/p&gt;

&lt;p&gt;I'd like to hear how other teams keep track of their apps today, especially anyone doing it per app rather than per warehouse.&lt;/p&gt;

</description>
      <category>snowflake</category>
      <category>dataengineering</category>
      <category>streamlit</category>
      <category>finops</category>
    </item>
    <item>
      <title>Why the Django admin says 403 behind a proxy, and the one line that fixes it</title>
      <dc:creator>John Bannister</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:06:50 +0000</pubDate>
      <link>https://dev.to/eudorajab1/why-the-django-admin-says-403-behind-a-proxy-and-the-one-line-that-fixes-it-5nf</link>
      <guid>https://dev.to/eudorajab1/why-the-django-admin-says-403-behind-a-proxy-and-the-one-line-that-fixes-it-5nf</guid>
      <description>&lt;p&gt;So you put a Django app behind a reverse proxy that handles HTTPS (nginx, Traefik, Caddy, a PaaS router, a load balancer), open &lt;code&gt;/admin/&lt;/code&gt;, type the right username and password, and get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forbidden (403)
CSRF verification failed. Request aborted.

Origin checking failed - https://myapp.example does not match any trusted origins.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The username and password are fine (checked umpteen times). Django just thinks the request came in over plain HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;The proxy normally ends HTTPS and passes the request on to Django over HTTP, adding a header that says what the original scheme was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Forwarded-Proto: https
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Django will ignore that header unless you tell it to trust it. So &lt;code&gt;request.is_secure()&lt;/code&gt; is &lt;code&gt;False&lt;/code&gt;, and Django expects the browsers &lt;code&gt;Origin&lt;/code&gt; to be &lt;code&gt;http://myapp.example&lt;/code&gt;. The browser sends &lt;code&gt;https://myapp.example&lt;/code&gt;, they don't match, and since Django 4.0 that mismatch is a 403 on every POST, including the admin login.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;settings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SECURE_PROXY_SSL_HEADER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP_X_FORWARDED_PROTO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Django treats requests with &lt;code&gt;X-Forwarded-Proto: https&lt;/code&gt; as secure, the expected origin becomes &lt;code&gt;https://...&lt;/code&gt;, and the login works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; only set this when your proxy always sets (or overwrites) &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; itself. If clients can reach Django directly and send that header themselves, they can make a plain HTTP request look secure. The Django docs say as much.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSRF_TRUSTED_ORIGINS: when you need it
&lt;/h2&gt;

&lt;p&gt;You don't need it for the case above. You need it when forms are posted from another origin (a separate frontend domain, for example), and since Django 4.0 the entries need the scheme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CSRF_TRUSTED_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://myapp.example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# not "myapp.example"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leaving out &lt;code&gt;https://&lt;/code&gt; is a common reason it "doesn't work" after an upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  While you're in settings.py: the database from the environment
&lt;/h2&gt;

&lt;p&gt;Most hosts hand you the database as environment variables, so no password has to sit in the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;DATABASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENGINE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.db.backends.postgresql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POSTGRES_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get one &lt;code&gt;DATABASE_URL&lt;/code&gt; instead, django-environ makes it one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;

&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;DATABASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And list &lt;code&gt;psycopg[binary]&lt;/code&gt; in your requirements, or the first &lt;code&gt;migrate&lt;/code&gt; fails on the missing driver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;403 "Origin checking failed" behind an HTTPS proxy: &lt;code&gt;SECURE_PROXY_SSL_HEADER&lt;/code&gt;, as long as the proxy controls &lt;code&gt;X-Forwarded-Proto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CSRF_TRUSTED_ORIGINS&lt;/code&gt; only for cross-origin posts, and always with &lt;code&gt;https://&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Database settings from the environment, not in the code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hit all of this while writing a step-by-step guide for Uppcoder, the platform I work on: &lt;a href="https://portal.uppcoder.com/guides/deploy-django-postgresql" rel="noopener noreferrer"&gt;Deploy a Django app with PostgreSQL, free&lt;/a&gt;. There's also a small starter project with these settings already in place: &lt;a href="https://github.com/websson/django-postgresql-starter" rel="noopener noreferrer"&gt;websson/django-postgresql-starter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hopefully this will save anyone facing the same or similar stack install some time and nerves.&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>webdev</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Four things that broke when we set up 5 opensource apps and cost us a day</title>
      <dc:creator>John Bannister</dc:creator>
      <pubDate>Wed, 21 Oct 2026 15:51:05 +0000</pubDate>
      <link>https://dev.to/eudorajab1/four-things-that-broke-when-we-set-up-5-opensource-apps-and-cost-us-a-day-3j0g</link>
      <guid>https://dev.to/eudorajab1/four-things-that-broke-when-we-set-up-5-opensource-apps-and-cost-us-a-day-3j0g</guid>
      <description>&lt;p&gt;This week we added a handful of open-source apps as one-click templates on our platform: Gitea, Wiki.js, HedgeDoc, Etherpad and Mattermost, each with its own PostgreSQL (our preferred db). All official images, nothing patched.&lt;/p&gt;

&lt;p&gt;Four of them broke in ways I couldn't find in any of their docs. None of this is specific to us so we thought we would share so that anyone else running or planning to run any of these in containers will not lose an afternoon, loads of hair and send the coffee bill through the roof.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Wiki.js: SQLITE_CANTOPEN, over and over
&lt;/h2&gt;

&lt;p&gt;The container starts, the health check is happy, and the log says this ten times before it gives up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loading configuration from /wiki/config.yml... OK
[MASTER] info: Connecting to database...
[MASTER] error: Database Connection Error: SQLITE_CANTOPEN undefined:undefined
[MASTER] warn: Will retry in 3 seconds... [Attempt 1 of 10]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image runs as the user &lt;code&gt;node&lt;/code&gt;, not root. When you mount a brand new named volume on a folder that doesn't exist in the image, like &lt;code&gt;/data&lt;/code&gt;, Docker creates it owned by root. So &lt;code&gt;node&lt;/code&gt; can't create the SQLite file.&lt;/p&gt;

&lt;p&gt;This is the compose that breaks (it's the one from an unanswered GitHub question, and it was ours too):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wiki&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/requarks/wiki:2.5&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlite&lt;/span&gt;
      &lt;span class="na"&gt;DB_FILEPATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data/db.sqlite3&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wiki-data:/data&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wiki-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this one works. &lt;code&gt;/wiki/data&lt;/code&gt; already exists in the image and belongs to &lt;code&gt;node&lt;/code&gt;, and Docker gives a new volume the same owner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wiki&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/requarks/wiki:2.5&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sqlite&lt;/span&gt;
      &lt;span class="na"&gt;DB_FILEPATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/wiki/data/db.sqlite3&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wiki-data:/wiki/data&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wiki-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already created the volume on &lt;code&gt;/data&lt;/code&gt;, remove it first, or it stays owned by root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;rm&lt;/span&gt; &amp;lt;project&amp;gt;_wiki-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the container as root also works, but was really not an option for us and you really don't need to. And for anything beyond trying it out, Wiki.js recommends PostgreSQL anyway, which is what we use now. (I answered the GitHub question here: &lt;a href="https://github.com/requarks/wiki/discussions/7106" rel="noopener noreferrer"&gt;requarks/wiki#7106&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  2. HedgeDoc 1.12 won't start if DATABASE_URL is set
&lt;/h2&gt;

&lt;p&gt;This one is just a plain bug. If you give HedgeDoc a &lt;code&gt;DATABASE_URL&lt;/code&gt; variable, even with &lt;code&gt;CMD_DB_URL&lt;/code&gt; set as well, it dies at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: uncaughtException: Cannot read properties of undefined (reading 'dbURL')
TypeError: Cannot read properties of undefined (reading 'dbURL')
    at Object.&amp;lt;anonymous&amp;gt; (/hedgedoc/lib/config/index.js:153:15)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line reproduces it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;CMD_DB_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sqlite:///tmp/db.sqlite &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;CMD_SESSION_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgres://u:p@localhost:5432/db quay.io/hedgedoc/hedgedoc:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take out &lt;code&gt;DATABASE_URL&lt;/code&gt; and the same command starts fine.&lt;/p&gt;

&lt;p&gt;What happens is the image ships an empty &lt;code&gt;config.json&lt;/code&gt;. &lt;code&gt;DATABASE_URL&lt;/code&gt; is read as an old-style setting, and the code that merges the old and new  settings then tries to find something in the file config that is not there.&lt;/p&gt;

&lt;p&gt;Why it matters: lots of hosts give every app a &lt;code&gt;DATABASE_URL&lt;/code&gt; (Heroku-style platforms do, and so do we), and on those HedgeDoc can't start at all. Until it's fixed, give the container &lt;code&gt;CMD_DB_URL&lt;/code&gt; only. I reported it with the repro: &lt;a href="https://github.com/hedgedoc/hedgedoc/issues/6905" rel="noopener noreferrer"&gt;hedgedoc/hedgedoc#6905&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Gitea's setup page 'guesses' the wrong address
&lt;/h2&gt;

&lt;p&gt;On first start, Gitea shows its install page with everything pre-filled, including "Gitea Website URL". It guesses that from whatever address the first request came in on. Ours sits behind a login proxy, so it picked up the proxy's redirect, with a &lt;code&gt;?_gate_...&lt;/code&gt; parameter on the end. Click Install like that and every clone link and every email carries the wrong address.&lt;/p&gt;

&lt;p&gt;Our preferred option is to set it before the first start instead, so in the gitea container's environment somethign like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;GITEA__server__ROOT_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://git.example.com/&lt;/span&gt;
&lt;span class="na"&gt;GITEA__server__DOMAIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two small things from the same day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;GITEA____APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My Git&lt;/span&gt;            &lt;span class="c1"&gt;# top-level settings take FOUR underscores; GITEA__APP_NAME is silently ignored&lt;/span&gt;
&lt;span class="na"&gt;GITEA__server__DISABLE_SSH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;   &lt;span class="c1"&gt;# if only HTTPS reaches the server, or the clone box offers SSH links that can't work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Mattermost: "This invite link is invalid"
&lt;/h2&gt;

&lt;p&gt;Invite people, Copy invite link, send it, and the new person gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This invite link is invalid
Please speak with your Administrator to receive an invitation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at Mattermost's API about the same link explains it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"api.team.get_invite_info.not_open_team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Invite is invalid because this is not an open team."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our test team was created using the API with type &lt;code&gt;I&lt;/code&gt; (invite only). The Public/Private choice in Team settings doesn't change that type, so the link never worked. A new team created in Mattermost's own screens gets type &lt;code&gt;O&lt;/code&gt; and its link works straight away. If you create teams through the API, send &lt;code&gt;"type": "O"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two more traps from the same afternoon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test the link in a private window. In a browser that's already signed in, it just opens the team as you, so it looks broken when it isn't.&lt;/li&gt;
&lt;li&gt;If the app sits behind a login (ours does while it's set to Private), the people you invite hit that login first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  That's it
&lt;/h2&gt;

&lt;p&gt;Those four cost us most of a day. We've built the fixes into our templates now, so this is how we run them: each app with its own PostgreSQL wired in, on its own HTTPS address. If you'd rather not deal with any of this yourself: &lt;a href="https://portal.uppcoder.com/apps/gitea" rel="noopener noreferrer"&gt;Gitea&lt;/a&gt;, &lt;a href="https://portal.uppcoder.com/apps/wiki-js" rel="noopener noreferrer"&gt;Wiki.js&lt;/a&gt;, &lt;a href="https://portal.uppcoder.com/apps/hedgedoc" rel="noopener noreferrer"&gt;HedgeDoc&lt;/a&gt;, &lt;a href="https://portal.uppcoder.com/apps/etherpad" rel="noopener noreferrer"&gt;Etherpad&lt;/a&gt;, &lt;a href="https://portal.uppcoder.com/apps/mattermost" rel="noopener noreferrer"&gt;Mattermost&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which app would you want next?&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>GitHub Classroom has closed: what you still have, and how to run next term</title>
      <dc:creator>John Bannister</dc:creator>
      <pubDate>Fri, 16 Oct 2026 15:32:57 +0000</pubDate>
      <link>https://dev.to/eudorajab1/github-classroom-has-closed-what-you-still-have-and-how-to-run-next-term-2h7h</link>
      <guid>https://dev.to/eudorajab1/github-classroom-has-closed-what-you-still-have-and-how-to-run-next-term-2h7h</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I work on &lt;a href="https://portal.uppcoder.com/github-classroom-alternative?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=posts&amp;amp;utm_content=post-100" rel="noopener noreferrer"&gt;Uppcoder&lt;/a&gt;, one of the options listed at the end. The rest is about GitHub Classroom itself and applies whatever you choose.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;GitHub retired GitHub Classroom on 28 August 2026. If your course ran on it, here is what is still there, what is gone, and what your choices are for the next term.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened, and when
&lt;/h2&gt;

&lt;p&gt;GitHub closed new sign-ups to GitHub Classroom in May and retired it at the end of August. It now points educators to two partners: Classroom 50, free and open source from the Fifty Foundation, the nonprofit behind CS50, and Codio, a commercial platform.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;26 May 2026: new sign-ups to GitHub Classroom closed&lt;/li&gt;
&lt;li&gt;28 August 2026: GitHub Classroom retired; its website, APIs and related services decommissioned&lt;/li&gt;
&lt;li&gt;GitHub's recommended partners: Classroom 50 and Codio&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you still have
&lt;/h2&gt;

&lt;p&gt;GitHub says that GitHub accounts, repositories and organisations created to work with Classroom are not affected. Everything that lived in a repository is still where it was.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your classroom organisation&lt;/li&gt;
&lt;li&gt;Every student repository Classroom created, with its full commit history&lt;/li&gt;
&lt;li&gt;Your template (starter) repositories&lt;/li&gt;
&lt;li&gt;Any tests kept inside a repository, for example in a tests folder or a GitHub Actions workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is gone, or going
&lt;/h2&gt;

&lt;p&gt;GitHub says some Classroom data is being permanently deleted under its retention policies. GitHub's Classroom Export Utility downloaded this data as JSON and CSV files, but it worked through the Classroom APIs, which GitHub says are now decommissioned. If you ran it before 28 August, keep those files; if you did not, plan to rebuild what is listed here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classroom and assignment names&lt;/li&gt;
&lt;li&gt;Tests defined in Classroom itself rather than in a repository (autograding set up on the Classroom website)&lt;/li&gt;
&lt;li&gt;LMS-integrated rosters: the LTI link between Classroom and Moodle, Canvas and the rest&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding your students' repositories
&lt;/h2&gt;

&lt;p&gt;Classroom named each student repository after the assignment and the student's GitHub username, so the repositories of one assignment share a prefix. With the GitHub CLI (gh) you can list them, and clone them all into one folder. The roster that matched usernames to students was Classroom data; rebuild it from your LMS class list or your own records if you need it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# list one assignment's repositories&lt;/span&gt;
gh repo list YOUR-ORG &lt;span class="nt"&gt;--limit&lt;/span&gt; 1000 &lt;span class="nt"&gt;--json&lt;/span&gt; name &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.[].name'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^assignment-prefix-'&lt;/span&gt;

&lt;span class="c"&gt;# clone them all into the current folder&lt;/span&gt;
gh repo list YOUR-ORG &lt;span class="nt"&gt;--limit&lt;/span&gt; 1000 &lt;span class="nt"&gt;--json&lt;/span&gt; name &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.[].name'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^assignment-prefix-'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nt"&gt;-I&lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt; gh repo clone YOUR-ORG/&lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Before next term: a checklist
&lt;/h2&gt;

&lt;p&gt;Whatever you use next, these steps keep last term's work safe and make the next one easier to set up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep the organisation. Archive last term's student repositories rather than deleting them, if your school's retention rules allow it&lt;/li&gt;
&lt;li&gt;Move tests into the template repositories, in a tests folder, so they travel with the assignment whatever tool you use next&lt;/li&gt;
&lt;li&gt;Write down the assignment list with each template repository and its due dates, since the names in Classroom are being deleted&lt;/li&gt;
&lt;li&gt;Check your LMS: if Classroom was linked to Moodle, Canvas, Blackboard or Brightspace, remove the old activity so students don't click into a dead end&lt;/li&gt;
&lt;li&gt;Pick the replacement before the term starts and try one assignment end to end with a test student account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The options for next term
&lt;/h2&gt;

&lt;p&gt;Each takes a different approach; which fits depends on your course, your school's rules on student data, and how much you want to run yourself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classroom 50 (free, open source, the Fifty Foundation): closest to GitHub Classroom, with assignments from template repositories, autograding through GitHub Actions, rosters and submissions, on GitHub. Needs an organisation on a GitHub Team or Enterprise plan (free for verified educators through GitHub Education) and a token with read and write access to the organisation's repositories&lt;/li&gt;
&lt;li&gt;Codio (commercial, a GitHub partner): a hosted platform for coding courses with its own content, environments and autograding, and migration guides for GitHub Classroom users&lt;/li&gt;
&lt;li&gt;Do it yourself with the GitHub CLI: for small courses, create the student repositories from a template with gh and a list of usernames, and mark by hand or with your own GitHub Actions; community tools shared in GitHub's own discussion of the change do this for you&lt;/li&gt;
&lt;li&gt;Uppcoder (that's us; commercial, with a free tier): assignments are git repositories with starter code, tests and a solution; every student works in the same ready environment in the cloud, so nobody installs anything; tests mark each hand-in and you mark the rest by rubric; hand-ins can stay in your own GitHub organisation; it works inside Moodle, Canvas, Blackboard and Brightspace over LTI 1.3, or on its own; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you choose Uppcoder:&lt;/strong&gt; for teachers moving from GitHub Classroom we move one course across for you, up to ten of its assignments, and it runs free until 30 June 2027 (first 20 courses, one per teacher, up to 40 students, apply by 15 December 2026). Details: &lt;a href="https://portal.uppcoder.com/github-classroom-alternative?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=posts&amp;amp;utm_content=post-100" rel="noopener noreferrer"&gt;https://portal.uppcoder.com/github-classroom-alternative?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=posts&amp;amp;utm_content=post-100&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.blog/changelog/2026-08-27-github-classroom-deprecated" rel="noopener noreferrer"&gt;GitHub Changelog, 27 August 2026: GitHub Classroom deprecated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/changelog/2026-05-26-github-classroom-sign-ups-are-no-longer-available" rel="noopener noreferrer"&gt;GitHub Changelog, 26 May 2026: GitHub Classroom sign-ups are no longer available&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/education/manage-coursework-with-github-classroom/get-started-with-github-classroom/export-or-migrate-github-classroom-data" rel="noopener noreferrer"&gt;GitHub Docs: Export or migrate GitHub Classroom data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/orgs/community/discussions/196615" rel="noopener noreferrer"&gt;GitHub Community: the educators' announcement thread&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>education</category>
      <category>teaching</category>
      <category>git</category>
    </item>
  </channel>
</rss>
